[TYPESCRIPT] React Props와 State 타입 정의 — 컴포넌트 안정성을 결정하는 핵심

 

React + TypeScript에서 가장 중요한 부분을 하나만 꼽으라면 단연 Props와 State 타입 정의입니다. 이 두 가지를 어떻게 정의하느냐에 따라 컴포넌트의 재사용성, 안정성, 리팩토링 난이도가 완전히 달라집니다.

 

 

Props와 State를 타입으로 정의해야 하는 이유

TypeScript를 쓰지 않는 React에서는 다음과 같은 문제가 자주 발생합니다.

  • Props 누락이나 오타를 런타임에서야 발견
  • 컴포넌트 사용 방법을 코드로 추측해야 함
  • 리팩토링 시 영향 범위 파악이 어려움

Props와 State를 타입으로 정의하면 컴포넌트의 사용 계약(Contract)이 코드에 명확히 드러나고, 잘못된 사용은 컴파일 단계에서 바로 차단됩니다.

 

Props 타입 정의의 기본

가장 기본적인 Props 타입 정의는 객체 타입으로 시작합니다.

type ButtonProps = {
  label: string;
  disabled?: boolean;
};

function Button({ label, disabled }: ButtonProps) {
  return (
    <button disabled={disabled}>
      {label}
    </button>
  );
}

여기서 핵심 포인트는 다음과 같습니다.

  • 필수 Props는 일반 프로퍼티
  • 선택적 Props는 ?로 명시
  • Props 구조 자체가 컴포넌트 설명서 역할

 

Props 타입은 interface vs type?

React Props에서는 typeinterface 모두 사용 가능합니다.

interface CardProps {
  title: string;
  children?: React.ReactNode;
}

실무 기준 정리:

  • 확장이 필요한 경우 → interface
  • 유니온, 조합 타입이 필요한 경우 → type

팀 컨벤션만 일관되게 유지하면 둘 중 무엇을 써도 무방합니다.

 

children Props 타입 정의

React에서 가장 자주 쓰이지만 헷갈리는 것이 children 타입입니다.

type LayoutProps = {
  children: React.ReactNode;
};

function Layout({ children }: LayoutProps) {
  return <div className="layout">{children}</div>;
}

React.ReactNode는 다음을 모두 포함합니다.

  • JSX 요소
  • 문자열, 숫자
  • null, undefined
  • 배열

따라서 children에는 대부분 React.ReactNode가 정답입니다.

 

State 타입 정의 — 기본 원칙

State는 컴포넌트 내부의 “변할 수 있는 데이터”입니다. TypeScript에서는 useState 제네릭을 통해 타입을 명확히 할 수 있습니다.

const [count, setCount] = useState(0);
// count: number (자동 추론)

초기값이 명확하다면 대부분 타입 추론으로 충분합니다.

 

null을 포함하는 State 타입

API 데이터나 비동기 로딩 상태에서는 null이 포함되는 경우가 많습니다.

type User = {
  id: number;
  name: string;
};

const [user, setUser] = useState<User | null>(null);

이렇게 하면 렌더링 시점에서 null 체크를 강제할 수 있습니다.

{user && <div>{user.name}</div>}

이는 런타임 오류를 막는 매우 중요한 패턴입니다.

 

객체 State와 부분 업데이트

객체를 State로 관리할 때는 타입 정의가 더욱 중요합니다.

type FormState = {
  email: string;
  password: string;
};

const [form, setForm] = useState<FormState>({
  email: "",
  password: "",
});

부분 업데이트 시에도 타입이 유지됩니다.

setForm(prev => ({
  ...prev,
  email: "test@example.com",
}));

타입 덕분에 잘못된 필드 업데이트가 컴파일 단계에서 차단됩니다.

 

Props + State를 함께 쓰는 실전 예제

type CounterProps = {
  initialValue?: number;
};

function Counter({ initialValue = 0 }: CounterProps) {
  const [count, setCount] = useState(initialValue);

  return (
    <div>
      <button onClick={() => setCount(c => c - 1)}>-</button>
      <span>{count}</span>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  );
}

이 컴포넌트의 계약은 매우 명확합니다.

  • Props: 초기값만 제어 가능
  • State: 내부에서만 변경

 

실무에서 자주 하는 실수

  • Props 타입을 any로 처리
  • State에 너무 많은 책임 부여
  • null 가능성을 타입에 반영하지 않음
  • 컴포넌트 외부 타입과 내부 State 타입을 혼용

대부분 “타입을 대충 정의한 것”에서 시작되는 문제들입니다.

 

실무 권장 정리

  • Props는 컴포넌트의 공개 API
  • State는 내부 구현 세부사항
  • Props/State 모두 타입으로 명확히 표현
  • null/optional 여부를 반드시 타입에 반영

 


 

React + TypeScript에서 Props와 State 타입 정의는 컴포넌트 설계 그 자체입니다. 타입을 잘 정의해두면 컴포넌트는 자연스럽게 문서화되고, 잘못된 사용은 컴파일 단계에서 차단됩니다.

  • Props = 외부 계약
  • State = 내부 상태
  • 타입 정의가 곧 설계