본문으로 건너뛰기

안동민 개발노트

본문 시작

React 컴포넌트와 타입스크립트 통합

React 함수·클래스 컴포넌트의 props·state·이벤트 타입을 선언해 컴포넌트 사용 계약을 컴파일 단계에서 검증합니다.

React는 선언적이고 컴포넌트 기반의 UI 개발을 위한 인기 있는 자바스크립트 라이브러리입니다.

UI를 작은 독립적인 조각, 즉 컴포넌트로 나누어 개발하고 조합하여 애플리케이션을 구축하는 방식은 복잡한 UI를 효율적으로 관리할 수 있게 해줍니다.

타입스크립트를 React 프로젝트에 통합하는 것은 여러 가지 이점을 제공합니다.

특히 컴포넌트의 Props와 State에 대한 강력한 타입 검사는 개발 과정에서 발생할 수 있는 많은 오류를 미리 방지하고, 코드의 가독성 및 유지보수성을 크게 향상시킵니다.

이 절에서는 React 컴포넌트를 타입스크립트와 함께 사용하는 기본적인 방법과 이점을 살펴보겠습니다.

React와 TypeScript 통합

컴포넌트 경계마다 입력, 내부 상태, 이벤트, 자식의 타입 계약을 명시하면 UI 조각 사이의 오류를 줄일 수 있습니다.

  1. 입력
    Props

    부모가 넘기는 데이터와 콜백의 모양을 인터페이스로 정의합니다.

  2. 내부 상태
    State

    useState가 저장할 값과 갱신 함수의 타입을 고정합니다.

  3. 상호작용
    Event

    클릭과 입력 핸들러가 받는 React 이벤트 객체를 명확히 합니다.

  4. 합성
    children

    ReactNode 등 컴포넌트가 받을 수 있는 자식 범위를 선언합니다.


React 프로젝트에 타입스크립트 적용하기

새로운 React 프로젝트를 타입스크립트와 함께 시작하는 가장 일반적인 방법은 Vite의 TypeScript 템플릿을 사용하는 것입니다.

npm create vite@latest my-react-ts-app -- --template react-ts
cd my-react-ts-app
npm install
npm run dev

이 명령어는 타입스크립트 설정(tsconfig.json), React와 관련된 타입 정의(@types/react, @types/react-dom 등)를 포함하여 바로 사용할 수 있는 React 프로젝트를 생성해줍니다.

기존 프로젝트에 타입스크립트를 추가하는 경우, 필요한 의존성을 수동으로 설치하고 .js 파일을 .tsx 파일로 변경해야 합니다.

npm install --save-dev typescript @types/react @types/react-dom @types/jest

함수형 컴포넌트

React 16.8부터 도입된 Hooks는 함수형 컴포넌트에서도 상태(State)와 생명주기(Lifecycle) 기능을 사용할 수 있게 하여, 이제 대부분의 React 컴포넌트는 함수형으로 작성됩니다.

타입스크립트와 함께 함수형 컴포넌트를 정의하는 방법은 다음과 같습니다.

Props 타입 정의

React 컴포넌트 타입은 Props, State, Event 경계를 나눈다

컴포넌트가 받는 값, 내부 상태, 사용자 상호작용 타입을 분리하면 UI 계약과 입력 처리가 안전해진다.

  1. 부모가 넘기는 입력

    Props 부모가 넘기는 입력 필수·선택 속성과 children 계약을 정의한다.

  2. 컴포넌트 내부 값

    State 컴포넌트 내부 값 초기값과 변경 가능한 범위를 일관되게 둔다.

  3. 사용자 상호작용

    Event 사용자 상호작용 ChangeEvent, MouseEvent처럼 DOM 이벤트 형태를 명시한다.

구분의미판단
props contract외부 입력 문서화잘못된 prop을 실행 전에 잡는다.
state unionloading/success/errorUI 분기를 타입으로 드러낸다.
event handler대상 요소 타입value 접근 실수를 줄인다.

함수형 컴포넌트의 Props는 함수 인자의 타입으로 정의됩니다.

보통 interfacetype 별칭을 사용하여 Props의 구조를 명확하게 선언합니다.

src/components/WelcomeMessage.tsx
import React from 'react';

// 1. Props 타입을 인터페이스로 정의
interface WelcomeMessageProps {
  name: string;
  age?: number; // 선택적 Props
  isVisible: boolean;
  onButtonClick: (message: string) => void; // 함수 Props
}

// 2. React.FC (Function Component) 타입을 사용하여 컴포넌트 정의
//    React.FC는 children을 기본적으로 포함하지만, 명시적으로 children을 관리할 때는 FC<Props> 대신
//    직접 Props 타입을 함수 인자로 사용하는 것을 권장 (TypeScript 18.0.0 React.FC의 변경사항)
//    FC는 React 18에서 암묵적인 children 타입 추론을 제거했습니다.
const WelcomeMessage: React.FC<WelcomeMessageProps> = ({ name, age, isVisible, onButtonClick }) => {
  if (!isVisible) {
    return null;
  }

  const handleClick = () => {
    onButtonClick(`Hello, ${name}!`);
  };

  return (
    <div>
      <h1>Hello, {name}!</h1>
      {age && <p>You are {age} years old.</p>}
      <button onClick={handleClick}>Say Hello</button>
    </div>
  );
};

export default WelcomeMessage;
React.FC 사용의 장단점
  • 장점: 컴포넌트의 반환 타입(JSX.Element | null)을 명시하고, defaultPropspropTypes와 같은 정적 속성을 정의할 때 편리합니다.
  • 단점: React 18 이전에는 children Prop이 암묵적으로 포함되어 타입 추론을 방해할 수 있었습니다. React 18부터는 React.FCchildren을 더 이상 암묵적으로 포함하지 않으므로, children을 사용하려면 Props 타입에 명시적으로 추가해야 합니다.
    • 권장되는 방식: React.FC 대신 직접 함수 인자에 Props 타입을 명시하는 것이 더 명확하고 유연합니다.
src/components/WelcomeMessageV2.tsx
import React from 'react';

interface WelcomeMessageProps {
  name: string;
  age?: number;
  isVisible: boolean;
  onButtonClick: (message: string) => void;
  children?: React.ReactNode; // children을 사용하려면 명시적으로 추가
}

// React.FC를 사용하지 않고 직접 Props 타입 명시 (권장)
const WelcomeMessageV2 = ({ name, age, isVisible, onButtonClick, children }: WelcomeMessageProps) => {
  if (!isVisible) {
    return null;
  }

  const handleClick = () => {
    onButtonClick(`Hello, ${name}!`);
  };

  return (
    <div>
      <h1>Hello, {name}!</h1>
      {age && <p>You are {age} years old.</p>}
      <button onClick={handleClick}>Say Hello</button>
      {children && <p>{children}</p>}
    </div>
  );
};

export default WelcomeMessageV2;

State 타입 정의 (useState)

useState 훅을 사용할 때 타입스크립트는 초기값을 통해 State의 타입을 추론합니다.

명시적으로 타입을 지정해줄 수도 있습니다.

src/components/Counter.tsx
import React, { useState } from 'react';

const Counter: React.FC = () => {
  // 1. 타입 추론에 의존: 초기값이 number이므로 count는 number 타입으로 추론됩니다.
  const [count, setCount] = useState(0);

  // 2. 명시적으로 타입 지정: 초기값이 null이거나 복잡한 객체일 때 유용합니다.
  interface User {
    id: number;
    name: string;
  }
  const [user, setUser] = useState<User | null>(null); // user는 User 또는 null 타입

  const increment = () => {
    setCount(prevCount => prevCount + 1);
  };

  const decrement = () => {
    setCount(prevCount => prevCount - 1);
  };

  const fetchUser = () => {
    // 비동기 작업 시뮬레이션
    setTimeout(() => {
      setUser({ id: 1, name: 'Alice' });
    }, 1000);
  };

  return (
    <div>
      <h2>Counter: {count}</h2>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>

      <h3>User Info:</h3>
      <button onClick={fetchUser}>Fetch User</button>
      {user ? (
        <p>User ID: {user.id}, Name: {user.name}</p>
      ) : (
        <p>No user data.</p>
      )}
    </div>
  );
};

export default Counter;

이벤트 핸들러 타입

HTML 엘리먼트의 이벤트 핸들러는 React가 제공하는 특정 이벤트 타입(React.MouseEvent, React.ChangeEvent 등)을 사용합니다.

src/components/InputField.tsx
import React, { useState } from 'react';

const InputField: React.FC = () => {
  const [value, setValue] = useState('');

  // ChangeEvent<HTMLInputElement> 타입 명시
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setValue(event.target.value);
  };

  // MouseEvent<HTMLButtonElement> 타입 명시
  const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    alert(`Current value: ${value}`);
  };

  return (
    <div>
      <input type="text" value={value} onChange={handleChange} placeholder="Type something..." />
      <button onClick={handleClick}>Show Value</button>
      <p>Input: {value}</p>
    </div>
  );
};

export default InputField;

클래스형 컴포넌트

클래스형 컴포넌트도 여전히 사용할 수 있으며, Props와 State 타입을 제네릭으로 명시하여 정의합니다.

src/components/ClassBasedCounter.tsx

import React, { Component } from 'react';

// Props 타입 정의
interface ClassBasedCounterProps {
  initialCount?: number;
  title: string;
}

// State 타입 정의
interface ClassBasedCounterState {
  count: number;
}

// Component<Props, State> 제네릭을 사용하여 타입 명시
class ClassBasedCounter extends Component<ClassBasedCounterProps, ClassBasedCounterState> {
  // defaultProps는 static 속성으로 정의하여 선택적 Props의 기본값을 제공
  static defaultProps = {
    initialCount: 0,
  };

  constructor(props: ClassBasedCounterProps) {
    super(props);
    this.state = {
      count: props.initialCount || 0, // defaultProps를 사용하더라도, constructor에서는 직접 접근해야 합니다.
    };
  }

  // 메서드도 타입을 명시할 수 있습니다.
  private increment = (): void => {
    this.setState(prevState => ({ count: prevState.count + 1 }));
  };

  private decrement = (): void => {
    this.setState(prevState => ({ count: prevState.count - 1 }));
  };

  render() {
    return (
      <div>
        <h2>{this.props.title}</h2>
        <h3>Count: {this.state.count}</h3>
        <button onClick={this.increment}>Increment</button>
        <button onClick={this.decrement}>Decrement</button>
      </div>
    );
  }
}

export default ClassBasedCounter;

컴포넌트 사용 예시

위에서 정의한 컴포넌트들을 메인 애플리케이션 파일에서 사용하는 방법입니다.

타입스크립트는 Props를 전달할 때 정의된 타입과 일치하는지 자동으로 검사해줍니다.

src/App.tsx
import React from 'react';
import WelcomeMessage from './components/WelcomeMessage';
import Counter from './components/Counter';
import ClassBasedCounter from './components/ClassBasedCounter';
import WelcomeMessageV2 from './components/WelcomeMessageV2'; // React.FC 대신 함수 인자에 Props 타입 직접 명시 버전

const App: React.FC = () => {
  const handleWelcomeButtonClick = (message: string) => {
    alert(`Button clicked! Message: ${message}`);
  };

  return (
    <div style={{ padding: '20px', fontFamily: 'Arial, sans-serif' }}>
      <h1>React with TypeScript Examples</h1>

      <hr />
      <h2>Welcome Message Component (React.FC)</h2>
      {/* 필수 Props 'name'과 'isVisible'을 제공해야 합니다. */}
      <WelcomeMessage
        name="TypeScript User"
        age={25}
        isVisible={true}
        onButtonClick={handleWelcomeButtonClick}
      />
      {/*
      // 컴파일 에러 예시: 필수 Props 누락
      <WelcomeMessage name="Guest" />
      // Property 'isVisible' is missing in type '{ name: string; }'
      // but required in type 'WelcomeMessageProps'.
      */}
      {/*
      // 컴파일 에러 예시: 잘못된 Props 타입
      <WelcomeMessage name={123} isVisible={true} onButtonClick={() => {}} />
      // Type 'number' is not assignable to type 'string'.
      */}

      <hr />
      <h2>Welcome Message Component (Function Component with explicit Props)</h2>
      <WelcomeMessageV2
        name="Explicit Type User"
        isVisible={true}
        onButtonClick={handleWelcomeButtonClick}
      >
        This is a children prop passed to WelcomeMessageV2.
      </WelcomeMessageV2>

      <hr />
      <h2>Counter Component (Functional)</h2>
      <Counter />

      <hr />
      <h2>Class-based Counter Component</h2>
      <ClassBasedCounter
        title="Class Counter Example"
        initialCount={100}
      />
    </div>
  );
};

export default App;

컴포넌트를 실제로 조합할 때 TypeScript는 부모가 넘기는 Props, 컴포넌트 내부 State, 이벤트 핸들러, 렌더링 결과의 계약을 차례로 확인합니다.

컴포넌트 타입은 호출부, 상태, 이벤트, JSX 결과를 한 계약으로 묶는다

Props는 부모 호출부에서, State와 Event는 컴포넌트 내부에서, JSX 결과는 렌더 경계에서 검증된다.

  1. Props

    부모가 넘기는 필수/선택 입력

  2. Default

    선택 입력을 내부 기본값으로 보강

  3. State

    초기값과 갱신값의 범위 유지

  4. Event

    대상 요소와 핸들러 시그니처 고정

  5. Render

    조건부 JSX의 반환 분기 확인

검사 지점타입 선언 위치주로 잡는 오류설계 기준
함수형 컴포넌트Props 타입, Hook 제네릭누락 Props, 잘못된 State 값Props 타입을 먼저 작게 정의한다
클래스 컴포넌트P / S 계약생성자와 render의 State 구조 불일치초기 State와 갱신 State를 같은 계약으로 둔다
이벤트 핸들러React 이벤트 타입대상 요소 오해, 값 접근 오류핸들러 매개변수를 명시한다
컴포넌트 조합부모 호출부와 자식 Props속성 이름 변경 누락리팩토링 시 호출 지점을 함께 추적한다
조건부 렌더union 상태와 반환 JSXloading/error/empty 상태 혼합화면 상태를 하나의 null 값으로 뭉개지 않는다

타입스크립트와 React 컴포넌트 통합의 이점

개발 단계에서의 오류 감지: Props를 잘못 전달하거나, State를 예상치 못한 타입으로 설정하는 등의 실수를 컴파일 단계에서 즉시 잡아낼 수 있습니다.

이는 런타임 오류로 이어질 수 있는 문제를 조기에 방지합니다.

강력한 자동 완성 및 리팩토링 지원: IDE는 Props와 State의 타입을 알고 있으므로, 컴포넌트 사용 시 자동 완성 기능을 제공하고, 속성 이름 변경 시 관련된 모든 코드에 대한 안전한 리팩토링을 지원합니다.

코드 가독성 및 의도 명확화: 컴포넌트가 어떤 Props를 기대하고, 어떤 타입의 값을 다루는지 명확하게 문서화됩니다.

이는 다른 개발자가 코드를 이해하고 사용하기 쉽게 만듭니다.

유지보수성 향상: 시간이 지남에 따라 컴포넌트의 요구사항이 변경될 때, 타입 정의를 업데이트함으로써 관련 코드의 모든 불일치를 빠르게 파악하고 수정할 수 있습니다.

협업 효율 증대: 팀원 간의 인터페이스 계약을 명확히 하여, 컴포넌트 간의 의존성을 줄이고 협업 시 발생할 수 있는 혼란을 최소화합니다.

React와 타입스크립트의 조합은 현대 웹 개발에서 매우 강력한 시너지를 발휘하며, 대규모 애플리케이션을 개발할 때 특히 그 가치가 빛을 발합니다.

React 컴포넌트 타입 경계

Props, State, Event, Render 결과를 각각 타입으로 고정하면 컴포넌트 사용 실수와 내부 상태 접근 오류가 실행 전에 드러납니다.

  1. 검증 경계

    필수 값, 선택 값, 콜백 매개변수, children 포함 여부를 선언합니다. WelcomeMessageProps null 초기값과 객체 상태는 제네릭으로 가능한 범위를 드러냅니다. useState<User | null> 입력과 버튼의 이벤트 타입을 나누어 target 접근을 안전하게 만듭니다. ChangeEvent<HTMLInputElement> 조건부 null 반환과 JSX 결과도 컴포넌트의 반환 계약에 포함됩니다. JSX.Element | null 부모 전달 Props 검사 상태 좁히기 렌더링

  2. Props

    필수 값, 선택 값, 콜백 매개변수, children 포함 여부를 선언합니다. WelcomeMessageProps

  3. State

    null 초기값과 객체 상태는 제네릭으로 가능한 범위를 드러냅니다. useState<User | null>

  4. Event

    입력과 버튼의 이벤트 타입을 나누어 target 접근을 안전하게 만듭니다. ChangeEvent<HTMLInputElement>

  5. Render

    조건부 null 반환과 JSX 결과도 컴포넌트의 반환 계약에 포함됩니다. JSX.Element | null

컴포넌트 타입은 한 번 정의하고 끝나는 문서가 아니라, Props 변경, State 분리, 이벤트 흐름이 바뀔 때마다 함께 갱신해야 하는 계약입니다.

컴포넌트 타입은 변경 루프마다 갱신한다

Props, State, 이벤트, 렌더링 결과가 서로 맞물리므로 UI 변경은 타입 계약 변경과 함께 검토합니다.

  1. 부모가 넘기는 값

    Props 필수값, 선택값, children을 컴포넌트의 공개 계약으로 둡니다.

  2. 내부 상태

    State 초기값이 비어 있으면 `null`과 로딩 상태를 명시합니다.

  3. 사용자 입력

    Event HTML 요소에 맞는 React 이벤트 타입으로 핸들러를 좁힙니다.

  4. 출력 결과

    Render 조건부 렌더링에서 `null`, 목록 key, 반환 형태를 확인합니다.

아래 다이어그램은 React 컴포넌트의 공개 Props, 내부 State, DOM 이벤트, 자식 콜백 경계를 나누어 보여줍니다.

컴포넌트 타입 경계

React에서 타입 안정성은 컴포넌트 하나의 Props뿐 아니라 이벤트와 콜백이 지나가는 모든 경계에서 만들어집니다.

  1. 부모와의 공개 계약

    Props 필수값, 선택값, children, callback을 컴포넌트의 사용 설명서처럼 둡니다. type Props

  2. 내부 변화 관리

    State 로딩, 성공, 실패처럼 화면 상태가 나뉘면 판별 유니언이 흐름을 명확히 합니다.

  3. 요소별 이벤트 좁히기

    DOM Event input, form, button에 맞는 React 이벤트 타입을 사용해 target 접근을 안전하게 합니다.

  4. 자식에서 부모로 신호 전달

    Callback 자식이 넘기는 값의 타입을 명확히 해야 부모의 상태 갱신도 흔들리지 않습니다.

아래 다이어그램은 TypeScript 적용과 함수형 컴포넌트 설계가 Props, State, 이벤트 타입 계약에 어떤 영향을 주는지 정리합니다.

Props 계약이 호출부에서 렌더 결과까지 이어진다

첫 매개변수 타입이 JSX 속성 계약이 되고, 상태와 콜백 타입이 내부 흐름을 잇는다.

  1. 1
    부모 JSX 호출

    <Profile user={user} onSave={save} /> 속성 검사 시작 필수·선택 속성과 값 타입을 확인한다.

  2. 2
    Props 매개변수

    function Profile(props: ProfileProps) 공개 계약 확정 첫 매개변수 타입이 속성의 기준이다.

  3. 3
    상태·콜백 전파

    ViewState = "idle" | "saving" onSave(id: string) 상태 전이와 부모로 돌려줄 값을 제한한다.

  4. 4
    JSX 결과 확인

    return <button>Save</button> JSX runtime 타입 반환 결과와 내부 요소도 JSX 타입으로 검사한다.

  5. 5
    부모가 결정

    Props required·optional·union으로 호출 조합을 제한한다.

  6. 6
    컴포넌트가 소유

    State 초기값이 부족하면 union을 명시한다.

  7. 7
    데이터가 역방향 이동

    Callback DOM 이벤트 대신 도메인 값을 노출한다.