React · 2025-12-13

TypeScript와 React로 타입 안전한 컴포넌트 만들기

TypeScript와 React를 결합해 컴포넌트의 props와 상태를 명확히 정의하는 방법과 예제를 통해 타입 안전성을 확보하는 실무 예제 모음

작성일 : 2025-12-13 ㆍ 작성자 : 관리자
post
목차

소개

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 컴포넌트 타입 정의에 초점을 맞춰 실무 적용이 쉬운 패턴을 보여준다. 처음에는 타입 선언이 번거롭게 느껴질 수 있지만, 점차 빠른 개발과 안정성으로 보상이 돌아온다.

react typescript 설정 리액트 타입스크립트 예제 tsx 컴포넌트 타입 정의 TypeScript React 타입 안전 컴포넌트 props 타입 제네릭 컴포넌트