icon
13장 : React와 타입스크립트

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


React는 선언적이고 컴포넌트 기반의 UI 개발을 위한 인기 있는 자바스크립트 라이브러리입니다. UI를 작은 독립적인 조각, 즉 컴포넌트로 나누어 개발하고 조합하여 애플리케이션을 구축하는 방식은 복잡한 UI를 효율적으로 관리할 수 있게 해줍니다.

타입스크립트를 React 프로젝트에 통합하는 것은 여러 가지 이점을 제공합니다. 특히 컴포넌트의 Props와 State에 대한 강력한 타입 검사는 개발 과정에서 발생할 수 있는 많은 오류를 미리 방지하고, 코드의 가독성 및 유지보수성을 크게 향상시킵니다. 이 절에서는 React 컴포넌트를 타입스크립트와 함께 사용하는 기본적인 방법과 이점을 살펴보겠습니다.


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

새로운 React 프로젝트를 타입스크립트와 함께 시작하는 가장 일반적인 방법은 Create React App (CRA)을 사용하는 것입니다.

npx create-react-app my-react-ts-app --template typescript
cd my-react-ts-app
npm start

이 명령어는 타입스크립트 설정(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 타입 정의

함수형 컴포넌트의 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} // 선택적 Props는 생략 가능
        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} // initialCount는 선택적이므로 생략 가능
      />
    </div>
  );
};

export default App;

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

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

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

코드 가독성 및 의도 명확화: 컴포넌트가 어떤 Props를 기대하고, 어떤 타입의 값을 다루는지 명확하게 문서화됩니다. 이는 다른 개발자가 코드를 이해하고 사용하기 쉽게 만듭니다.

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

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

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