From f9d20e89464b31941fb164e188b7503a7c7f5e20 Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Sun, 23 Aug 2026 21:50:47 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20setState=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=95=88=EC=97=90=EC=84=9C=20event.currentTarget?= =?UTF-8?q?=20=EC=9D=84=20=EC=9D=BD=EC=A7=80=20=EC=95=8A=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 프로젝트 편집 화면에서 활동 유형이나 제목을 한 글자 치면 화면이 통째로 죽었다 — "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) Claude-Session: https://claude.ai/code/session_01XEHXspz4rv5pB5wiiSsVDu --- eslint.config.ts | 14 ++++ .../studio/components/project-editor.tsx | 69 ++++++++++--------- 2 files changed, 50 insertions(+), 33 deletions(-) diff --git a/eslint.config.ts b/eslint.config.ts index 5287876..090e6d2 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -594,6 +594,20 @@ const commonSecurityRules = { "CallExpression[callee.object.name='document'][callee.property.name='createElement'][arguments.0.value='script']", 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.", + }, ], }; diff --git a/src/features/tech-log/presentation/studio/components/project-editor.tsx b/src/features/tech-log/presentation/studio/components/project-editor.tsx index 4678754..b6c58a0 100644 --- a/src/features/tech-log/presentation/studio/components/project-editor.tsx +++ b/src/features/tech-log/presentation/studio/components/project-editor.tsx @@ -495,12 +495,12 @@ export function ProjectEditor() { 유형 - setNewActivity((current) => ({ - ...current, - occurredAt: event.currentTarget.value, - })) - } + onChange={(event) => { + // setState 업데이터는 다음 렌더에 실행된다. 그때 `event.currentTarget` 은 이미 + // null 이므로, 값은 핸들러가 도는 동안 꺼내 둬야 한다. + const value = event.currentTarget.value; + setNewActivity((current) => ({ ...current, occurredAt: value })); + }} />