fix: setState 업데이터 안에서 event.currentTarget 을 읽지 않는다
프로젝트 편집 화면에서 활동 유형이나 제목을 한 글자 치면 화면이 통째로 죽었다 — "Cannot read properties of null (reading 'value')". setState 업데이터는 핸들러가 끝난 뒤 다음 렌더에 실행된다. 그때 React 는 이미 `event.currentTarget` 을 null 로 되돌려 놓았으므로, 업데이터 안에서 그것을 읽으면 반드시 터진다. 타입 검사도 lint 도 이것을 잡지 못했고, 첫 입력에서야 드러났다. 값은 핸들러가 도는 동안 지역 변수로 꺼내 두고 업데이터에는 그 값을 넘긴다. 같은 실수를 다시 못 하도록 lint 규칙을 세운다: `setXxx(...)` 에 곧바로 넘기는 화살표 함수 안에서는 `currentTarget` 을 읽을 수 없다. 처음 쓴 선택자는 동기 핸들러까지 잡아 (map 콜백, 즉시 호출되는 update 등 아홉 자리) 너무 넓었으므로, 실제로 위험한 setter 업데이터만 겨냥하도록 좁혔다. 결함을 되돌려 규칙이 잡는 것을 확인했다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XEHXspz4rv5pB5wiiSsVDu
This commit is contained in:
co-authored by
Claude Opus 5
parent
014f21b9e1
commit
f9d20e8946
@@ -594,6 +594,20 @@ const commonSecurityRules = {
|
|||||||
"CallExpression[callee.object.name='document'][callee.property.name='createElement'][arguments.0.value='script']",
|
"CallExpression[callee.object.name='document'][callee.property.name='createElement'][arguments.0.value='script']",
|
||||||
message: "Runtime script construction is prohibited by FE-OC-019.",
|
message: "Runtime script construction is prohibited by FE-OC-019.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
setState 업데이터는 핸들러가 끝난 뒤, 다음 렌더에 실행된다. 그때 React 는 이미
|
||||||
|
`event.currentTarget` 을 null 로 되돌려 놓았으므로 업데이터 안에서 그것을 읽으면
|
||||||
|
"Cannot read properties of null" 로 화면이 통째로 죽는다.
|
||||||
|
|
||||||
|
타입 검사도 lint 도 잡지 못했고, 첫 입력에서야 드러났다 — 값은 핸들러가 도는 동안
|
||||||
|
지역 변수로 꺼내 두고 업데이터에는 그 값을 넘긴다.
|
||||||
|
*/
|
||||||
|
selector:
|
||||||
|
"CallExpression[callee.name=/^set[A-Z]/] > ArrowFunctionExpression MemberExpression[property.name='currentTarget']",
|
||||||
|
message:
|
||||||
|
"Read event.currentTarget before the setState updater runs — it is null by the time the updater is called.",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -495,12 +495,12 @@ export function ProjectEditor() {
|
|||||||
<span>유형</span>
|
<span>유형</span>
|
||||||
<select
|
<select
|
||||||
value={newActivity.activityType}
|
value={newActivity.activityType}
|
||||||
onChange={(event) =>
|
onChange={(event) => {
|
||||||
setNewActivity((current) => ({
|
// setState 업데이터는 다음 렌더에 실행된다. 그때 `event.currentTarget` 은 이미
|
||||||
...current,
|
// null 이므로, 값은 핸들러가 도는 동안 꺼내 둬야 한다.
|
||||||
activityType: event.currentTarget.value,
|
const value = event.currentTarget.value;
|
||||||
}))
|
setNewActivity((current) => ({ ...current, activityType: value }));
|
||||||
}
|
}}
|
||||||
>
|
>
|
||||||
{ACTIVITY_TYPES.map((type) => (
|
{ACTIVITY_TYPES.map((type) => (
|
||||||
<option key={type.value} value={type.value}>
|
<option key={type.value} value={type.value}>
|
||||||
@@ -514,12 +514,12 @@ export function ProjectEditor() {
|
|||||||
<input
|
<input
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
value={newActivity.occurredAt}
|
value={newActivity.occurredAt}
|
||||||
onChange={(event) =>
|
onChange={(event) => {
|
||||||
setNewActivity((current) => ({
|
// setState 업데이터는 다음 렌더에 실행된다. 그때 `event.currentTarget` 은 이미
|
||||||
...current,
|
// null 이므로, 값은 핸들러가 도는 동안 꺼내 둬야 한다.
|
||||||
occurredAt: event.currentTarget.value,
|
const value = event.currentTarget.value;
|
||||||
}))
|
setNewActivity((current) => ({ ...current, occurredAt: value }));
|
||||||
}
|
}}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="studio-field studio-field--wide">
|
<label className="studio-field studio-field--wide">
|
||||||
@@ -527,9 +527,12 @@ export function ProjectEditor() {
|
|||||||
<input
|
<input
|
||||||
value={newActivity.title}
|
value={newActivity.title}
|
||||||
maxLength={200}
|
maxLength={200}
|
||||||
onChange={(event) =>
|
onChange={(event) => {
|
||||||
setNewActivity((current) => ({ ...current, title: event.currentTarget.value }))
|
// setState 업데이터는 다음 렌더에 실행된다. 그때 `event.currentTarget` 은 이미
|
||||||
}
|
// null 이므로, 값은 핸들러가 도는 동안 꺼내 둬야 한다.
|
||||||
|
const value = event.currentTarget.value;
|
||||||
|
setNewActivity((current) => ({ ...current, title: value }));
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="studio-field studio-field--wide">
|
<label className="studio-field studio-field--wide">
|
||||||
@@ -537,24 +540,24 @@ export function ProjectEditor() {
|
|||||||
<input
|
<input
|
||||||
value={newActivity.summary}
|
value={newActivity.summary}
|
||||||
maxLength={300}
|
maxLength={300}
|
||||||
onChange={(event) =>
|
onChange={(event) => {
|
||||||
setNewActivity((current) => ({
|
// setState 업데이터는 다음 렌더에 실행된다. 그때 `event.currentTarget` 은 이미
|
||||||
...current,
|
// null 이므로, 값은 핸들러가 도는 동안 꺼내 둬야 한다.
|
||||||
summary: event.currentTarget.value,
|
const value = event.currentTarget.value;
|
||||||
}))
|
setNewActivity((current) => ({ ...current, summary: value }));
|
||||||
}
|
}}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="studio-field">
|
<label className="studio-field">
|
||||||
<span>연결할 기록</span>
|
<span>연결할 기록</span>
|
||||||
<select
|
<select
|
||||||
value={newActivity.relatedId}
|
value={newActivity.relatedId}
|
||||||
onChange={(event) =>
|
onChange={(event) => {
|
||||||
setNewActivity((current) => ({
|
// setState 업데이터는 다음 렌더에 실행된다. 그때 `event.currentTarget` 은 이미
|
||||||
...current,
|
// null 이므로, 값은 핸들러가 도는 동안 꺼내 둬야 한다.
|
||||||
relatedId: event.currentTarget.value,
|
const value = event.currentTarget.value;
|
||||||
}))
|
setNewActivity((current) => ({ ...current, relatedId: value }));
|
||||||
}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">연결하지 않음</option>
|
<option value="">연결하지 않음</option>
|
||||||
{records.map((entry) => (
|
{records.map((entry) => (
|
||||||
@@ -571,12 +574,12 @@ export function ProjectEditor() {
|
|||||||
<span>노출</span>
|
<span>노출</span>
|
||||||
<select
|
<select
|
||||||
value={newActivity.visibility}
|
value={newActivity.visibility}
|
||||||
onChange={(event) =>
|
onChange={(event) => {
|
||||||
setNewActivity((current) => ({
|
// setState 업데이터는 다음 렌더에 실행된다. 그때 `event.currentTarget` 은 이미
|
||||||
...current,
|
// null 이므로, 값은 핸들러가 도는 동안 꺼내 둬야 한다.
|
||||||
visibility: event.currentTarget.value,
|
const value = event.currentTarget.value;
|
||||||
}))
|
setNewActivity((current) => ({ ...current, visibility: value }));
|
||||||
}
|
}}
|
||||||
>
|
>
|
||||||
<option value="PUBLIC">공개</option>
|
<option value="PUBLIC">공개</option>
|
||||||
<option value="PRIVATE">비공개</option>
|
<option value="PRIVATE">비공개</option>
|
||||||
|
|||||||
Reference in New Issue
Block a user