React 교재 · 고급 20편

React + TypeScript — 컴포넌트 타입

1편부터 줄곧 TS 로 왔다. 이제 React 전용 타입 7 패턴을 한 번에 정리.

타입 툴팁이 떠 있는 React 컴포넌트 코드 일러스트 — TypeScript 통합 컨셉

2편에서 --template react-ts 로 시작했고, 이후 모든 예제가 .tsx. 이번 20편은 그 동안 흩어져 있던 React + TypeScript 패턴 7가지를 한 곳에 모아 정리. 컴포넌트 작성하다 "이 타입 어떻게 쓰지" 막힐 때 찾는 레퍼런스로.

1. Props — type 또는 interface

// type (현재 React 커뮤니티 우세) type ButtonProps = { label: string; variant?: 'primary' | 'secondary'; onClick: () => void; }; function Button({ label, variant = 'primary', onClick }: ButtonProps) { return <button className={variant} onClick={onClick}>{label}</button>; } // interface 도 거의 동일하게 사용 가능 interface ButtonProps { ... }

실전 선택 — props 는 type 이 권장. & 로 교차 타입·literal union 등 더 유연. interface 는 동일 이름으로 선언 병합 가능 (라이브러리 확장에 유리). 둘 다 알아두되 일반 컴포넌트엔 type.

2. children — React.ReactNode 가 정답

type CardProps = { title: string; children: React.ReactNode; // 텍스트·JSX·배열·null 모두 허용 }; function Card({ title, children }: CardProps) { return <div><h2>{title}</h2><div>{children}</div></div>; }

흔한 실수 — JSX.Element 로 타입 박기. 그러면 string·null·배열을 못 받음. 거의 항상 ReactNode.

3. useState — 명시적 제네릭이 필요한 경우

// 추론 가능 — 명시 불필요 const [count, setCount] = useState(0); // number const [name, setName] = useState(''); // string // 추론 불가 — 명시 필요 const [user, setUser] = useState<User | null>(null); const [items, setItems] = useState<string[]>([]);

초기값이 null 또는 빈 배열이면 TS 가 정확한 타입을 추론 못 함. 제네릭으로 알려준다. 안 그러면 setItems('hello') 같은 잘못된 호출도 통과.

4. 이벤트 핸들러 — React.MouseEvent · React.ChangeEvent

function Input() { const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { console.log(e.target.value); // ✅ string }; const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => { console.log(e.currentTarget.dataset.id); }; return <> <input onChange={handleChange} /> <button onClick={handleClick}>클릭</button> </>; }

제네릭에 이벤트가 발생한 요소의 HTML 타입을 넣음. HTMLInputElement·HTMLButtonElement·HTMLFormElement·HTMLSelectElement 같은 표준 DOM 타입.

핸들러를 props 로 받을 때onClick: (e: React.MouseEvent<HTMLButtonElement>) => void 길다. 간단히 onClick: () => void 도 가능 (이벤트 객체 안 쓰면). 호출자에서 무엇이 필요한지에 따라 단순화.

ref — useRef 의 두 형태

// 1. DOM ref const inputRef = useRef<HTMLInputElement>(null); <input ref={inputRef} /> inputRef.current?.focus(); // ?. 필수 (null 가능) // 2. 값 보관용 ref (재렌더 트리거 X) const renderCount = useRef(0); renderCount.current += 1;

5. 제네릭 컴포넌트 — 재사용 컴포넌트의 진가

List·Select 같은 "어떤 타입의 데이터든 받아 그리는" 컴포넌트.

type ListProps<T> = { items: T[]; renderItem: (item: T) => React.ReactNode; }; function List<T>({ items, renderItem }: ListProps<T>) { return <ul>{items.map((item, i) => <li key={i}>{renderItem(item)}</li>)}</ul>; } // 사용 — T 가 User 로 자동 추론 <List items={users} renderItem={(user) => <strong>{user.name}</strong>} // user 가 User 타입 />

이 패턴 익히면 라이브러리 코드 (TanStack Table, react-hook-form 등) 의 시그니처가 읽힌다.

흔한 함정 — any · as · @ts-ignore

피해야 할 3가지 — ① any 타입 (TS 의 모든 이점 무효화). ② 무분별한 as 강제 캐스팅 (런타임 오류 가능). ③ @ts-ignore 주석 (에러 숨김). 정 안 되면 unknown + 타입 가드, 또는 라이브러리 .d.ts 보완. 회사 코드베이스 PR 리뷰에서 가장 자주 reject 되는 패턴.

20편으로 React + TS 의 모든 일상 패턴 정리. 21편부터는 production 단계 — 테스트. Vitest + Testing Library 로 React 컴포넌트 테스트 자동화.

다음 글

React 교재 21편 — Vitest + Testing Library. 컴포넌트 unit 테스트, 이벤트 시뮬레이션, async 검증.

© 2026 주나이테크(주) @JUNAITECH