props와 useState, 이벤트 타입을 다루고 타입 단언·제네릭 활용과 흔한 실수를 정리한다.
실전 React와 TypeScript
이제 배운 타입 지식을 실제 현장에서 가장 많이 쓰는 React에 적용해 봅니다. props, state, 이벤트에 타입을 입히는 기본기와, 자주 마주치는 실수를 함께 정리합니다.
학습 목표
- 함수 컴포넌트의 props에 타입을 붙인다.
useState에 타입을 지정한다.- 이벤트 핸들러의 타입을 안다.
- 타입 단언과 제네릭을 적절히 활용하고 흔한 실수를 피한다.
props 타입 정의
props는 interface 나 type 으로 정의해 컴포넌트에 넘깁니다.
interface ButtonProps {
label: string;
disabled?: boolean; // 선택적 prop
onClick: () => void;
}
function Button({ label, disabled, onClick }: ButtonProps) {
return (
<button disabled={disabled} onClick={onClick}>
{label}
</button>
);
}
💡 TIP — 자식 요소를 받을 땐
children: React.ReactNode를 props에 추가합니다. 텍스트·요소·배열을 모두 포괄하는 타입입니다.
useState 타입
대부분 초깃값에서 타입이 추론됩니다.
const [count, setCount] = useState(0); // number로 추론
const [name, setName] = useState(''); // string으로 추론
초깃값만으로는 부족할 때(예: 처음엔 null)는 제네릭으로 명시합니다.
interface User {
id: number;
name: string;
}
const [user, setUser] = useState<User | null>(null);
⚠️ 주의 —
useState(null)만 쓰면 타입이null로 고정돼 나중에 객체를 넣지 못합니다. 나중에 다른 값이 들어온다면useState<User | null>(null)처럼 유니온으로 명시하세요.
이벤트 타입
이벤트 객체에는 React가 제공하는 전용 타입을 씁니다.
function Form() {
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log(e.target.value);
}
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
}
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} />
</form>
);
}
| 이벤트 | 타입 |
|---|---|
| 입력 변경 | React.ChangeEvent<HTMLInputElement> |
| 클릭 | React.MouseEvent<HTMLButtonElement> |
| 폼 제출 | React.FormEvent<HTMLFormElement> |
| 키 입력 | React.KeyboardEvent<HTMLInputElement> |
💡 TIP — 핸들러를 JSX 속성에 인라인으로 적으면(
onChange={(e) => ...}) 타입이 자동 추론되어 직접 적을 필요가 없습니다. 함수를 따로 빼낼 때만 위 타입을 명시하면 됩니다.
타입 단언
TypeScript가 모르는 정보를 개발자가 단언할 때 as 를 씁니다. DOM 조회처럼 구체 타입을 알 때 유용합니다.
const input = document.getElementById('email') as HTMLInputElement;
input.value = 'a@b.com';
⚠️ 주의 —
as는 컴파일러를 "믿게" 만들 뿐 실제 검사를 하지 않습니다. 틀리면 런타임 오류로 이어집니다. 가능하면typeof/instanceof같은 narrowing을 먼저 시도하고,as는 정말 필요한 곳에만 쓰세요.as any는 특히 피합니다.
제네릭 활용 — 재사용 컴포넌트
리스트처럼 어떤 데이터에도 동작해야 하는 컴포넌트는 제네릭으로 만들면 타입이 보존됩니다.
interface ListProps<T> {
items: T[];
render: (item: T) => React.ReactNode;
}
function List<T>({ items, render }: ListProps<T>) {
return <ul>{items.map((item, i) => <li key={i}>{render(item)}</li>)}</ul>;
}
// 사용 시 item이 string으로 추론됨
<List items={['a', 'b']} render={(s) => s.toUpperCase()} />;
흔한 실수 정리
any남용: 타입 안전성을 통째로 잃습니다. 모르면unknown+ narrowing.useState(null)만 쓰기: 유니온으로 미래 값을 함께 선언하세요.as남발: 검사를 우회할 뿐입니다. narrowing을 우선하세요.- props를
any로 받기: 컴포넌트 계약이 사라집니다. 반드시 타입을 정의하세요. - 옵셔널 체이닝 누락:
user?.name으로null가능성을 안전하게 다루세요.
요약
- props는
interface/type으로 정의하고, 자식은React.ReactNode. useState는 추론에 맡기되,null등은 제네릭으로 타입을 명시한다.- 이벤트는
React.ChangeEvent<...>같은 전용 타입을 쓴다. as단언은 최소화하고 narrowing을 우선하며,any남용을 피한다.- 재사용 컴포넌트는 제네릭으로 타입을 보존한다.
댓글 0
“TypeScript” 강좌에 대한 댓글입니다.