{"slug":"react-expert","title":"react-expert","summary":"React 컴포넌트를 설계·구현·리팩터링하거나 상태 관리, useEffect 남용, 리렌더 성능, 접근성 문제를 다룰 때 사용한다. React 19 기준.","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-24T15:42:51.853887Z","repo":{"url":"https://github.com/LeeYudok/doksam-skills","stars":12,"forks":2,"license":"MIT","updatedAt":"2026-09-24T05:35:27Z"},"bodyHtml":"<hr>\n<h2>name: react-expert\ndescription: React 컴포넌트를 설계·구현·리팩터링하거나 상태 관리, useEffect 남용, 리렌더 성능, 접근성 문제를 다룰 때 사용한다. React 19 기준.</h2>\n<h1>react-expert</h1>\n<p>컴포넌트 코드가 대상이다. 번들러·패키지 매니저·의존성은 <code>frontend-build</code>,\n디자인 토큰·컴포넌트 선택은 <code>doksam-ui</code> 가 맡는다.</p>\n<p>이 문서는 <strong>일반론을 적지 않는다.</strong> 판단이 갈리는 지점, 자주 틀리는 곳, React 19 에서\n바뀐 것만 담는다.</p>\n<h2>1. 상태는 필요한 만큼만, 있어야 할 곳에</h2>\n<p>판단 순서:</p>\n<ol>\n<li><strong>props 나 기존 상태에서 계산할 수 있는가</strong> → 렌더 중에 계산한다. <code>useState</code> + <code>useEffect</code>\n조합으로 파생값을 동기화하지 않는다. 이 패턴이 버그의 큰 축이다.</li>\n<li><strong>여러 컴포넌트가 공유하는가</strong> → 가장 가까운 공통 부모로 올린다. 전역 스토어는\n\"여러 화면이 같은 서버 상태를 본다\"가 성립할 때만.</li>\n<li><strong>URL 에 있어야 하는가</strong> — 새로고침·공유·뒤로가기가 의미 있으면 라우터 상태다.\n상세 화면·필터·탭이 여기 해당한다.</li>\n</ol>\n<pre><code>// 나쁨 — 파생값을 상태로 두고 동기화\nconst [filtered, setFiltered] = useState&lt;Room[]&gt;([])\nuseEffect(() =&gt; { setFiltered(rooms.filter(r =&gt; r.name.includes(q))) }, [rooms, q])\n\n// 좋음 — 렌더 중 계산\nconst filtered = useMemo(() =&gt; rooms.filter(r =&gt; r.name.includes(q)), [rooms, q])\n</code></pre>\n<p><code>useMemo</code> 는 <strong>측정 가능한 비용이 있을 때만</strong>. 배열 몇 개 도는 것에 붙이면 코드만 늘어난다.</p>\n<h2>2. useEffect 는 \"외부 시스템과 동기화\"에만</h2>\n<p>effect 를 쓰기 전에 답한다: <strong>이 코드가 맞물리려는 외부 시스템이 무엇인가?</strong>\n(네트워크, DOM 이벤트, 타이머, 구독) 답이 없으면 effect 가 아니다.</p>\n<ul>\n<li><strong>사용자 행동의 결과는 이벤트 핸들러에서 처리한다.</strong> 상태를 바꾸고 그 변화를 effect 로\n감지해 후속 작업을 하는 구조는 흐름을 끊고 중복 실행을 부른다.</li>\n<li><strong>StrictMode 에서 effect 는 두 번 실행된다.</strong> 이건 버그가 아니라 정리(cleanup) 누락을\n드러내는 장치다. 두 번 돌아 깨지면 effect 쪽을 고친다.</li>\n</ul>\n<h3>비동기 요청 취소는 필수</h3>\n<pre><code>useEffect(() =&gt; {\n  let alive = true\n  api.messages(dbRef, roomId).then(m =&gt; { if (alive) setMessages(m) })\n  return () =&gt; { alive = false }\n}, [dbRef, roomId])\n</code></pre>\n<p>빠뜨리면 대상을 연달아 바꿀 때 <strong>먼저 보낸 응답이 나중에 도착해 화면을 덮는다</strong>(경합).\n<code>AbortController</code> 를 쓸 수 있으면 그쪽이 더 낫다 — 요청 자체를 끊는다.</p>\n<h3>의존성 배열을 거짓말로 채우지 않는다</h3>\n<p>린트가 요구하는 값을 빼서 \"한 번만 실행\"을 흉내내지 않는다. 대신 원인을 없앤다 —\n함수는 <code>useCallback</code> 으로 안정화하거나 effect 안으로 옮기고, 정말 마운트 1회면\n그 사실이 드러나게 쓴다.</p>\n<h2>3. 리스트와 key</h2>\n<ul>\n<li><code>key</code> 는 <strong>데이터의 안정적 식별자</strong>. 배열 인덱스는 순서가 바뀌거나 중간 삽입이 있으면\n상태가 엉뚱한 행에 붙는다.</li>\n<li><strong>컴포넌트를 초기화하고 싶을 때 <code>key</code> 를 바꾸는 것은 정식 기법이다.</strong>\n상세 뷰에서 대상이 바뀔 때 내부 상태를 리셋하는 가장 단순한 방법이다.</li>\n</ul>\n<pre><code>&lt;MessageView key={`${room.id}:${jumpTo ?? 0}`} ... /&gt;\n</code></pre>\n<h2>4. React 19 에서 달라진 것</h2>\n<ul>\n<li><strong><code>forwardRef</code> 가 필요 없다</strong> — 함수 컴포넌트가 <code>ref</code> 를 일반 prop 으로 받는다.\n기존 코드를 일괄 변환할 필요는 없지만 새 코드에서 쓰지 않는다.</li>\n<li><strong><code>use()</code></strong> 로 promise·context 를 조건부로 읽을 수 있다. Suspense 경계와 함께 쓴다.</li>\n<li><code>useFormStatus</code>·<code>useActionState</code> 는 폼 제출 상태를 다룬다. 서버 액션이 없는\nSPA 에서도 쓸 수 있다.</li>\n<li>ref 콜백이 정리 함수를 반환할 수 있다.</li>\n</ul>\n<p><strong>서드파티 컴포넌트에 ref 를 넘겨 특정 자식으로 스크롤하는 식의 조작은 취약하다.</strong>\n내부가 어떤 엘리먼트를 렌더하는지에 의존하기 때문이다. 이럴 땐 <code>data-*</code> 속성을 붙이고\n컨테이너에서 <code>querySelector</code> 로 찾는 편이 타입·구조 양쪽에서 안전하다.</p>\n<h2>5. 접근성 — 구조로 강제한다</h2>\n<p>리뷰에서 지적하는 대신 <strong>틀리기 어렵게</strong> 만든다.</p>\n<ul>\n<li><strong>아이콘 전용 버튼은 <code>aria-label</code> 이 없으면 만들지 않는다.</strong> 레이블을 필수 prop 으로 받는\n래퍼 컴포넌트를 두면 구조적으로 막힌다.</li>\n<li>클릭 가능한 것은 <code>&lt;button&gt;</code>/<code>&lt;a&gt;</code>. <code>&lt;div onClick&gt;</code> 은 키보드·스크린리더에서 사라진다.</li>\n<li>폼 입력은 <code>&lt;label htmlFor&gt;</code> 로 연결한다. placeholder 는 레이블이 아니다.</li>\n<li>에러 메시지는 <code>role=\"alert\"</code>.</li>\n<li>포커스 링을 지우지 않는다. 디자인상 바꿔야 하면 <code>focus-visible</code> 로 대체 스타일을 준다.</li>\n<li>상태를 색으로만 알리지 않는다 — 텍스트·아이콘·모양을 함께 쓴다.</li>\n<li>브라우저 <code>confirm()</code>/<code>alert()</code> 를 쓰지 않는다. 데스크톱 셸(웹뷰)에서 이벤트 루프를 막아\n앱이 굳는다. 2단계 인라인 확인이나 다이얼로그 컴포넌트로 대체한다.</li>\n</ul>\n<h2>6. 위험한 렌더</h2>\n<ul>\n<li><strong><code>dangerouslySetInnerHTML</code> 는 기본 금지.</strong> 꼭 필요하면 서버에서 새니타이즈하고, 그 사실을\n주석에 남긴다. 사용자 입력·외부 데이터를 그대로 넣지 않는다.</li>\n<li>외부에서 온 URL 을 <code>href</code>/<code>src</code> 에 넣을 때 스킴을 검사한다(<code>javascript:</code> 차단).</li>\n<li>사용자 텍스트는 JSX 텍스트 노드로 넣으면 자동 이스케이프된다 — 굳이 직렬화하지 않는다.</li>\n</ul>\n<h2>7. 에러 표면</h2>\n<ul>\n<li>서버가 주는 실패 사유를 <strong>삼키지 않는다.</strong> 공통 인터셉터(401 → 로그인 유도 등)를 둘 때는\n그 처리가 <strong>적용되면 안 되는 요청</strong>을 먼저 정한다. 로그인 요청의 401 은 \"세션 만료\"가\n아니라 \"자격 불일치\"이므로 서버 메시지가 그대로 보여야 한다.</li>\n<li>사용자에게 보여줄 메시지와 로그용 상세를 구분한다.</li>\n</ul>\n<h2>8. 성능은 측정 후에 손댄다</h2>\n<p>기본은 \"단순하게 쓰고, 느려지면 고친다\". React 컴파일러가 도입된 프로젝트라면\n수동 메모이제이션을 먼저 걷어낸다.</p>\n<p>실제로 문제가 되는 것들:</p>\n<ul>\n<li><strong>리스트가 길다</strong> → 가상 스크롤. 수백 건부터 체감된다.</li>\n<li><strong>큰 트리가 매 입력마다 리렌더</strong> → 상태를 아래로 내리거나 입력을 지역화한다.</li>\n<li><strong>검색·필터가 매 타이핑마다 요청</strong> → 디바운스(200~300ms). 이전 요청 취소도 함께.</li>\n<li><strong>부모가 매 렌더마다 새 객체·함수를 props 로 준다</strong> → 자식이 memo 여도 소용없다.</li>\n</ul>\n<h2>9. 화면 및 폼 인터랙션 검증 (Playwright / 접근성 트리)</h2>\n<ul>\n<li><strong>스크린샷 대신 접근성 스냅샷 우선</strong>: AI 에이전트나 자동화 도구로 UI를 검증할 때, 고비용 이미지 비교보다 <code>aria-controls</code>, <code>aria-expanded</code>, <code>role=\"alert\"</code> 등 접근성 트리(Accessibility Tree)를 기반으로 DOM 상태와 노출 여부를 판정한다.</li>\n<li><strong>접힘(Accordion/Collapsible) 영역 내 폼 상태</strong>: <code>hidden</code> 속성으로 숨겨진 폼 필드는 DOM에 남아 있어 자동완성을 보존하지만 렌더 트리에서는 비가시 상태다. 테스트 시 버튼 트리거로 가시 상태(<code>isVisible</code>) 전환 후 제출과 에러 안내 렌더링을 확인한다.</li>\n</ul>\n<h2>10. 완료 조건</h2>\n<ul>\n<li>타입체크 통과, <code>any</code> 0건</li>\n<li>파생값을 상태로 두고 effect 로 동기화하는 코드가 없음</li>\n<li>모든 비동기 effect 에 취소·정리가 있음</li>\n<li>아이콘 전용 버튼에 접근 가능한 이름이 있음</li>\n<li>리스트 <code>key</code> 가 안정적 식별자임</li>\n<li><code>dangerouslySetInnerHTML</code> 을 썼다면 근거가 주석에 있음</li>\n<li>실제로 띄워서 확인함 — 렌더 결과와 콘솔 에러 없음까지</li>\n</ul>\n<h2>Learned warnings</h2>\n<ul>\n<li>(2026-08-23) Playwright E2E/MCP 검증 시 이미지보다 접근성 트리(<code>role=alert</code>, <code>aria-expanded</code> 등)를 확인하는 것이 토큰 효율과 판정 정확도가 높다. 접힌(hidden) 폼 영역은 가시성 토글 이벤트 후 인터랙션을 검증한다.</li>\n</ul>\n","files":[{"path":"agents/antigravity.md","sizeBytes":569,"isText":true},{"path":"agents/claude.md","sizeBytes":425,"isText":true},{"path":"agents/codex.toml","sizeBytes":387,"isText":true},{"path":"agents/openai.yaml","sizeBytes":259,"isText":true},{"path":"SKILL.md","sizeBytes":8255,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-24T15:43:28.167032Z","sha256":"A7C95DD1FDB3F9A0E9DAAA1CCBB1ED7CE29BCCFBC45671118D258CBE2F4DE80D","sizeBytes":6027},"review":null,"source":{"repositoryUrl":"https://github.com/LeeYudok/doksam-skills","path":"skills/react-expert","license":"MIT","commit":"841cccdaa8b607e9c5d0adc9c95151c286a8f229","subtreeSha":"47D4C5BDEE620654F235D07D7B69A035B6218D7FA66E401B83187B8C474E410A","lastSyncedAt":"2026-09-24T15:42:49.340415Z"},"reviewedAt":"2026-09-24T15:45:18.276899Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/LeeYudok/doksam-skills/tree/main/skills/react-expert"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install leeyudok-doksam-skills@llmmart"},{"target":"git","command":"git clone https://github.com/LeeYudok/doksam-skills.git"}]}