TypeScript와 React로 타입 안전한 컴포넌트 만들기
TypeScript와 React를 결합해 컴포넌트의 props와 상태를 명확히 정의하는 방법과 예제를 통해 타입 안전성을 확보하는 실무 예제 모음
목차
소개
React와 TypeScript 조합은 코드 안정성을 크게 향상시킨다. 컴포넌트의 입력값을 명확히 정의하면 런타임 에러를 줄일 수 있다. 이하 내용은 처음 접하는 개발자도 이해하기 쉬운 흐름으로 구성되어 있다. 핵심은 props와 state의 타입 정의, 이벤트 핸들링, 제네릭 사용법, 그리고 실용적인 예제다.
환경 설정
프로젝트 초기화
리액트 타입스크립트 환경은 create-react-app 또는 Vite로 빠르게 구성된다. 기본 설정에서 타입 관련 패키지를 추가하면 개발 환경이 완성된다. 다음은 Vite 기반의 최소 설정 예시다.
{
"npm init vite@latest my-app -- --template react-ts"
}
tsconfig와 타입 패키지
tsconfig.json은 JSX와 모듈 해석을 조정한다. React 프로젝트에 적절한 옵션을 설정하면 편리하다. 또한 React 타입 정의가 필요하다.
{
"compilerOptions": {
"target": "ES2019",
"module": "ESNext",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
기본 컴포넌트 타입 정의
Props 타입 인터페이스
Props를 인터페이스로 정의하면 컴포넌트의 계약이 분명해진다. 다음 예제는 문자열 제목과 선택적 콜백을 받는 간단한 컴포넌트다.
{
type TitleProps = {
title: string;
onClose?: () => void;
};
const Title = ({ title, onClose }: TitleProps) => {
return (
<div>
<h1>{title}</h1>
{onClose && <button onClick={onClose}>닫기</button>}
</div>
);
};
export default Title;
}
JSX의 '<'와 '>'은 위 예제처럼 이스케이프 처리되어야 한다. 이렇게 하면 타입 추론과 자동 완성이 큰 도움이 된다.
함수형 컴포넌트와 제네릭
재사용 가능한 리스트 컴포넌트
데이터 타입이 유동적인 컴포넌트에는 제네릭이 유용하다. 다음은 리스트 항목 타입을 제네릭으로 받는 예제다.
{
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>
);
}
export default List;
}
이벤트와 폼 타입 처리
타입 안전한 이벤트 핸들러
이벤트 객체는 라이브러리별 타입을 사용하면 안전하다. React의 폼 이벤트 타입을 활용한 예제는 아래와 같다.
{
import React from 'react';
type InputProps = {
value: string;
onChange: (v: string) => void;
};
const TextInput = ({ value, onChange }: InputProps) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value);
};
return <input value={value} onChange={handleChange} />;
};
export default TextInput;
}
유니언과 좁혀가기
상태가 여러 형태일 때
상태 값이 여러 형식일 경우 유니언 타입과 타입 가드를 사용하면 안전하다. 예를 들어 로딩, 성공, 오류 상태를 명확히 표현하면 컴포넌트 내부 분기가 단순해진다.
{
type FetchState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
function renderState<T>(state: FetchState<T>) {
if (state.status === 'loading') return <p>로딩 중...</p>;
if (state.status === 'error') return <p>오류: {state.error}</p>;
if (state.status === 'success') return <div>{JSON.stringify(state.data)}</div>;
return <p>대기 중</p>;
}
}
실무 팁과 주의점
- strict 모드 활성화는 장기적으로 버그를 줄이는 데 도움이 된다.
- any 남용을 피하면 타입 안전성이 유지된다.
- 외부 라이브러리와 연동 시 타입 선언을 확인한다.
- 테스트 시 타입을 함께 검토하면 리팩터링이 쉬워진다.
결론
TypeScript를 도입하면 리액트 컴포넌트의 의도가 명확해진다. props와 state를 타입으로 정리하면 협업이 수월해진다. 위 예제들은 react typescript 설정과 리액트 타입스크립트 예제, tsx 컴포넌트 타입 정의에 초점을 맞춰 실무 적용이 쉬운 패턴을 보여준다. 처음에는 타입 선언이 번거롭게 느껴질 수 있지만, 점차 빠른 개발과 안정성으로 보상이 돌아온다.