React 컴포넌트를 설계·구현·리팩터링하거나 상태 관리, useEffect 남용, 리렌더 성능, 접근성 문제를 다룰 때 사용한다. React 19 기준.
Scanned 9/22/2026
Install to Claude Code
npx -y skills add LeeYudok/doksam-skills --skill react-expert --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of React Expert?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/leeyudok-react-expert)More formats (shields.io, HTML) on the badges page.
---
name: react-expert
description: React 컴포넌트를 설계·구현·리팩터링하거나 상태 관리, useEffect 남용, 리렌더 성능, 접근성 문제를 다룰 때 사용한다. React 19 기준.
---
# react-expert
컴포넌트 코드가 대상이다. 번들러·패키지 매니저·의존성은 `frontend-build`,
디자인 토큰·컴포넌트 선택은 `doksam-ui` 가 맡는다.
이 문서는 **일반론을 적지 않는다.** 판단이 갈리는 지점, 자주 틀리는 곳, React 19 에서
바뀐 것만 담는다.
## 1. 상태는 필요한 만큼만, 있어야 할 곳에
판단 순서:
1. **props 나 기존 상태에서 계산할 수 있는가** → 렌더 중에 계산한다. `useState` + `useEffect`
조합으로 파생값을 동기화하지 않는다. 이 패턴이 버그의 큰 축이다.
2. **여러 컴포넌트가 공유하는가** → 가장 가까운 공통 부모로 올린다. 전역 스토어는
"여러 화면이 같은 서버 상태를 본다"가 성립할 때만.
3. **URL 에 있어야 하는가** — 새로고침·공유·뒤로가기가 의미 있으면 라우터 상태다.
상세 화면·필터·탭이 여기 해당한다.
```tsx
// 나쁨 — 파생값을 상태로 두고 동기화
const [filtered, setFiltered] = useState<Room[]>([])
useEffect(() => { setFiltered(rooms.filter(r => r.name.includes(q))) }, [rooms, q])
// 좋음 — 렌더 중 계산
const filtered = useMemo(() => rooms.filter(r => r.name.includes(q)), [rooms, q])
```
`useMemo` 는 **측정 가능한 비용이 있을 때만**. 배열 몇 개 도는 것에 붙이면 코드만 늘어난다.
## 2. useEffect 는 "외부 시스템과 동기화"에만
effect 를 쓰기 전에 답한다: **이 코드가 맞물리려는 외부 시스템이 무엇인가?**
(네트워크, DOM 이벤트, 타이머, 구독) 답이 없으면 effect 가 아니다.
- **사용자 행동의 결과는 이벤트 핸들러에서 처리한다.** 상태를 바꾸고 그 변화를 effect 로
감지해 후속 작업을 하는 구조는 흐름을 끊고 중복 실행을 부른다.
- **StrictMode 에서 effect 는 두 번 실행된다.** 이건 버그가 아니라 정리(cleanup) 누락을
드러내는 장치다. 두 번 돌아 깨지면 effect 쪽을 고친다.
### 비동기 요청 취소는 필수
```tsx
useEffect(() => {
let alive = true
api.messages(dbRef, roomId).then(m => { if (alive) setMessages(m) })
return () => { alive = false }
}, [dbRef, roomId])
```
빠뜨리면 대상을 연달아 바꿀 때 **먼저 보낸 응답이 나중에 도착해 화면을 덮는다**(경합).
`AbortController` 를 쓸 수 있으면 그쪽이 더 낫다 — 요청 자체를 끊는다.
### 의존성 배열을 거짓말로 채우지 않는다
린트가 요구하는 값을 빼서 "한 번만 실행"을 흉내내지 않는다. 대신 원인을 없앤다 —
함수는 `useCallback` 으로 안정화하거나 effect 안으로 옮기고, 정말 마운트 1회면
그 사실이 드러나게 쓴다.
## 3. 리스트와 key
- `key` 는 **데이터의 안정적 식별자**. 배열 인덱스는 순서가 바뀌거나 중간 삽입이 있으면
상태가 엉뚱한 행에 붙는다.
- **컴포넌트를 초기화하고 싶을 때 `key` 를 바꾸는 것은 정식 기법이다.**
상세 뷰에서 대상이 바뀔 때 내부 상태를 리셋하는 가장 단순한 방법이다.
```tsx
<MessageView key={`${room.id}:${jumpTo ?? 0}`} ... />
```
## 4. React 19 에서 달라진 것
- **`forwardRef` 가 필요 없다** — 함수 컴포넌트가 `ref` 를 일반 prop 으로 받는다.
기존 코드를 일괄 변환할 필요는 없지만 새 코드에서 쓰지 않는다.
- **`use()`** 로 promise·context 를 조건부로 읽을 수 있다. Suspense 경계와 함께 쓴다.
- `useFormStatus`·`useActionState` 는 폼 제출 상태를 다룬다. 서버 액션이 없는
SPA 에서도 쓸 수 있다.
- ref 콜백이 정리 함수를 반환할 수 있다.
**서드파티 컴포넌트에 ref 를 넘겨 특정 자식으로 스크롤하는 식의 조작은 취약하다.**
내부가 어떤 엘리먼트를 렌더하는지에 의존하기 때문이다. 이럴 땐 `data-*` 속성을 붙이고
컨테이너에서 `querySelector` 로 찾는 편이 타입·구조 양쪽에서 안전하다.
## 5. 접근성 — 구조로 강제한다
리뷰에서 지적하는 대신 **틀리기 어렵게** 만든다.
- **아이콘 전용 버튼은 `aria-label` 이 없으면 만들지 않는다.** 레이블을 필수 prop 으로 받는
래퍼 컴포넌트를 두면 구조적으로 막힌다.
- 클릭 가능한 것은 `<button>`/`<a>`. `<div onClick>` 은 키보드·스크린리더에서 사라진다.
- 폼 입력은 `<label htmlFor>` 로 연결한다. placeholder 는 레이블이 아니다.
- 에러 메시지는 `role="alert"`.
- 포커스 링을 지우지 않는다. 디자인상 바꿔야 하면 `focus-visible` 로 대체 스타일을 준다.
- 상태를 색으로만 알리지 않는다 — 텍스트·아이콘·모양을 함께 쓴다.
- 브라우저 `confirm()`/`alert()` 를 쓰지 않는다. 데스크톱 셸(웹뷰)에서 이벤트 루프를 막아
앱이 굳는다. 2단계 인라인 확인이나 다이얼로그 컴포넌트로 대체한다.
## 6. 위험한 렌더
- **`dangerouslySetInnerHTML` 는 기본 금지.** 꼭 필요하면 서버에서 새니타이즈하고, 그 사실을
주석에 남긴다. 사용자 입력·외부 데이터를 그대로 넣지 않는다.
- 외부에서 온 URL 을 `href`/`src` 에 넣을 때 스킴을 검사한다(`javascript:` 차단).
- 사용자 텍스트는 JSX 텍스트 노드로 넣으면 자동 이스케이프된다 — 굳이 직렬화하지 않는다.
## 7. 에러 표면
- 서버가 주는 실패 사유를 **삼키지 않는다.** 공통 인터셉터(401 → 로그인 유도 등)를 둘 때는
그 처리가 **적용되면 안 되는 요청**을 먼저 정한다. 로그인 요청의 401 은 "세션 만료"가
아니라 "자격 불일치"이므로 서버 메시지가 그대로 보여야 한다.
- 사용자에게 보여줄 메시지와 로그용 상세를 구분한다.
## 8. 성능은 측정 후에 손댄다
기본은 "단순하게 쓰고, 느려지면 고친다". React 컴파일러가 도입된 프로젝트라면
수동 메모이제이션을 먼저 걷어낸다.
실제로 문제가 되는 것들:
- **리스트가 길다** → 가상 스크롤. 수백 건부터 체감된다.
- **큰 트리가 매 입력마다 리렌더** → 상태를 아래로 내리거나 입력을 지역화한다.
- **검색·필터가 매 타이핑마다 요청** → 디바운스(200~300ms). 이전 요청 취소도 함께.
- **부모가 매 렌더마다 새 객체·함수를 props 로 준다** → 자식이 memo 여도 소용없다.
## 9. 화면 및 폼 인터랙션 검증 (Playwright / 접근성 트리)
- **스크린샷 대신 접근성 스냅샷 우선**: AI 에이전트나 자동화 도구로 UI를 검증할 때, 고비용 이미지 비교보다 `aria-controls`, `aria-expanded`, `role="alert"` 등 접근성 트리(Accessibility Tree)를 기반으로 DOM 상태와 노출 여부를 판정한다.
- **접힘(Accordion/Collapsible) 영역 내 폼 상태**: `hidden` 속성으로 숨겨진 폼 필드는 DOM에 남아 있어 자동완성을 보존하지만 렌더 트리에서는 비가시 상태다. 테스트 시 버튼 트리거로 가시 상태(`isVisible`) 전환 후 제출과 에러 안내 렌더링을 확인한다.
## 10. 완료 조건
- 타입체크 통과, `any` 0건
- 파생값을 상태로 두고 effect 로 동기화하는 코드가 없음
- 모든 비동기 effect 에 취소·정리가 있음
- 아이콘 전용 버튼에 접근 가능한 이름이 있음
- 리스트 `key` 가 안정적 식별자임
- `dangerouslySetInnerHTML` 을 썼다면 근거가 주석에 있음
- 실제로 띄워서 확인함 — 렌더 결과와 콘솔 에러 없음까지
## Learned warnings
- (2026-08-23) Playwright E2E/MCP 검증 시 이미지보다 접근성 트리(`role=alert`, `aria-expanded` 등)를 확인하는 것이 토큰 효율과 판정 정확도가 높다. 접힌(hidden) 폼 영역은 가시성 토글 이벤트 후 인터랙션을 검증한다.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!