Frontend/Typescript

React + TypeScript: Props, State, Event Handler에 타입 적용하기

미니임 2026. 2. 20. 22:07

 

React 프로젝트에 TypeScript를 도입하면 컴포넌트 간의 데이터 흐름을 명확하게 정의할 수 있고, 런타임 에러를 사전에 방지할 수 있습니다. 가장 기본이 되는 Props, State, 그리고 Event Handler에 타입을 적용하는 방법을 풍부한 예제와 함께 알아보겠습니다.

1. 컴포넌트 Props에 타입 적용하기

Props는 부모 컴포넌트로부터 전달받는 데이터이므로, 인터페이스(interface)를 사용하여 그 구조를 정의하는 것이 정석입니다.

기초 예제: 기본 Props 정의

interface GreetingProps {
  name: string;
  age?: number; // 선택적 Props
  isLoggedIn: boolean;
}

// React.FC를 사용하는 방식 (최근에는 권장되지 않기도 함)
const Greeting: React.FC<GreetingProps> = ({ name, age, isLoggedIn }) => {
  return (
    <div>
      <h1>안녕하세요, {name}님!</h1>
      {isLoggedIn && <p>나이: {age ?? '비공개'}</p>}
    </div>
  );
};

// 또는 직접 구조 분해 할당에 타입을 지정하는 방식 (현재 권장되는 방식)
function Welcome({ name, age }: GreetingProps) {
  return <h1>환영합니다, {name}({age})님!</h1>;
}

고급 예제: children과 스타일 Props

interface ContainerProps {
  children: React.ReactNode; // React 요소 전체 (문자열, 컴포넌트 등)
  style?: React.CSSProperties; // 인라인 스타일 타입
}

const Card = ({ children, style }: ContainerProps) => {
  return <div style={{ border: '1px solid #ddd', ...style }}>{children}</div>;
};

2. useState (State)에 타입 적용하기

useState는 초기값을 통해 타입을 추론하지만, 복잡한 객체나 유니온 타입이 필요한 경우 제네릭(<T>)을 사용해야 합니다.

실무 예제: 단순 타입 vs 복잡한 타입

import { useState } from 'react';

interface User {
  id: number;
  username: string;
  email: string;
}

const UserProfile = () => {
  // 1. 초기값으로 타입이 추론되는 경우 (number)
  const [count, setCount] = useState(0);

  // 2. 유니온 타입이 필요한 경우
  const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');

  // 3. 객체 또는 인터페이스가 필요한 경우 (초기값이 null일 수 있음)
  const [user, setUser] = useState<User | null>(null);

  const login = () => {
    setUser({ id: 1, username: 'react_master', email: 'test@example.com' });
  };

  return (
    <div>
      <p>현재 상태: {status}</p>
      {user ? <p>사용자: {user.username}</p> : <button onClick={login}>로그인</button>}
    </div>
  );
};

3. 이벤트 핸들러(Event Handler) 타입 정의하기

이벤트 핸들러는 가장 실수가 잦은 부분입니다. React에서 제공하는 합성 이벤트(SyntheticEvent) 타입을 정확히 사용해야 합니다.

실무 예제: 입력 폼과 클릭 이벤트

import React, { useState } from 'react';

const ContactForm = () => {
  const [email, setEmail] = useState('');

  // 1. Change 이벤트 (Input, Textarea)
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setEmail(e.target.value);
  };

  // 2. Form Submit 이벤트
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    console.log('전송된 이메일:', email);
  };

  // 3. Mouse Click 이벤트
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    console.log('버튼 위치:', e.clientX, e.clientY);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" value={email} onChange={handleChange} />
      <button type="submit" onClick={handleClick}>
        제출하기
      </button>
    </form>
  );
};

4. useRef와 타이머 타입

DOM 요소에 접근하거나 리렌더링과 무관한 값을 저장할 때 사용하는 useRef 타입 지정법입니다.

실무 예제: 포커스 제어

import { useRef, useEffect } from 'react';

const AutoFocusInput = () => {
  // DOM 요소의 경우 HTML 요소를 제네릭으로 전달
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    // optional chaining을 통해 안전하게 접근
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} type="text" placeholder="자동 포커스" />;
};

요약

React에서 TypeScript를 효과적으로 활용하기 위한 핵심 요약입니다.

항목타입 지정 방법비고

Props interface 정의 후 컴포넌트에 적용 children은 React.ReactNode
useState useState<Type>(initialValue) 초기값으로 추론 가능하면 생략 가능
Input Change React.ChangeEvent<HTMLInputElement> 요소에 맞는 HTML 타입 지정 필요
Form Submit React.FormEvent<HTMLFormElement> e.preventDefault() 사용 시 필수
useRef (DOM) useRef<HTMLDivElement>(null) 초기값은 보통 null로 설정

TypeScript는 처음에는 번거로워 보일 수 있지만, 컴포넌트가 복잡해질수록 "내가 만든 컴포넌트를 남이(혹은 미래의 내가) 어떻게 사용해야 하는가"를 알려주는 가장 친절한 문서가 됩니다.

반응형