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 검증.