리액트에서 TypeScript를 활용해 props, state, 이벤트, API 응답 타입을 안전하게 다루는 방법을 정리합니다.
타입스크립트를 리액트에 적용하면 props 오타, undefined 접근, 잘못된 함수 인자 같은 실수를 빌드 단계에서 미리 잡을 수 있어요. 이번 수업에서는 실무에서 가장 자주 마주치는 패턴을 정리합니다.
컴포넌트가 받는 props를 interface로 정의하는 게 기본입니다.
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
variant?: "primary" | "outline" | "ghost";
}
export function Button({
label,
onClick,
disabled = false,
variant = "primary",
}: ButtonProps) {
return (
<button onClick={onClick} disabled={disabled} className={variant}>
{label}
</button>
);
}Tip
?를 붙이면 선택 prop이 됩니다. 기본값은 구조 분해 할당에서 = 값으로 설정하면 가장 깔끔합니다.
children을 받는 컴포넌트는 ReactNode 타입을 씁니다.
import type { ReactNode } from "react";
interface CardProps {
title: string;
children: ReactNode;
}
export function Card({ title, children }: CardProps) {
return (
<div className="card">
<h3>{title}</h3>
<div>{children}</div>
</div>
);
}Info
ReactNode는 문자열, 숫자, JSX, 배열, null 등 리액트가 렌더링할 수 있는 모든 것을 포함합니다. 가장 넓은 범위의 children 타입이에요.
초기값이 명확하면 자동 추론되지만, null이거나 빈 배열일 때는 직접 명시해야 합니다.
// 자동 추론 — 초기값에서 타입을 알 수 있을 때
const [count, setCount] = useState(0); // number 추론
// 직접 명시 — null 가능성이 있을 때
const [user, setUser] = useState<User | null>(null);
// 직접 명시 — 빈 배열은 타입을 알 수 없어서 반드시 명시
const [list, setList] = useState<string[]>([]);이벤트 핸들러를 함수로 분리하면 타입을 명시해야 합니다.
import type { ChangeEvent, FormEvent } from "react";
function LoginForm() {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
};
return (
<form onSubmit={handleSubmit}>
<input type="text" onChange={handleChange} />
<button type="submit">로그인</button>
</form>
);
}자주 하는 실수
e: Event로 쓰면 e.target.value에서 타입 오류가 납니다. 반드시 ChangeEvent<HTMLInputElement>처럼 구체적인 타입을 써야 합니다.
서버에서 받아오는 데이터는 타입을 직접 정의해서 씁니다.
interface Post {
id: number;
title: string;
author: {
name: string;
email: string;
};
tags: string[];
publishedAt: string | null;
}
async function fetchPost(id: number): Promise<Post> {
const res = await fetch(`/api/posts/${id}`);
if (!res.ok) throw new Error("포스트를 불러올 수 없습니다.");
return res.json() as Promise<Post>;
}중요
res.json()은 기본적으로 any를 반환합니다. as Promise<Post>로 캐스팅하거나, zod 같은 라이브러리로 런타임 검증까지 하는 것이 안전합니다.
타입스크립트는 처음엔 번거롭게 느껴지지만, 컴포넌트가 많아질수록 진가를 발휘합니다. props 타입만 잘 정의해도 팀원이 어떤 값을 넣어야 하는지 자동완성으로 바로 알 수 있거든요.