본문 바로가기
C.W.K.
Stream
Lesson 02 of 07 · published

타입이 있는 props와 컴포넌트 계약

~16 min · typescript, props, generics

Level 0React 입문자
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
타입이 붙은 props는 컴포넌트 작성자와 호출자 사이의 계약이야. 계약을 잘 세우면 컴파일러와 IDE가 실수를 먼저 잡아 줘.

Prop 타입을 선언하는 방법

작은 컴포넌트라면 함수 인자에 객체 타입을 바로 적어도 돼. 여러 곳에서 재사용하거나 export해야 한다면 이름 붙인 type이나 interface가 읽기 좋아. Union, intersection, mapped type을 조합해야 할 때는 type이 특히 유연하고, 외부 패키지가 선언을 확장해야 한다면 interface의 declaration merging이 도움이 돼. HOC 같은 헬퍼는 함수 시그니처에서 prop 타입을 추론하기도 해.

children에는 ReactNode를 써

레이아웃 컴포넌트는 보통 children: React.ReactNode를 받아. 문자열, 숫자, JSX, Fragment, 배열, nullundefined까지 React가 렌더링할 수 있는 값을 포괄해. 단일 문자열만 받아야 하는 명확한 이유가 없다면 더 좁은 타입으로 정상적인 사용을 막지 마.

네이티브 엘리먼트의 prop을 이어받아

직접 만든 Button이 onClick, aria-label, disabled 같은 네이티브 button prop도 받아야 한다면 하나씩 다시 선언하지 마. React.ComponentPropsWithoutRef<'button'>를 확장하면 브라우저 button의 계약을 그대로 가져올 수 있어.

제네릭 컴포넌트는 입력에서 타입을 추론해

<List items={users} renderItem={(user) => ...} />처럼 만들면 TypeScript가 items에서 항목 타입을 추론할 수 있어. TSX에서는 <T>가 JSX로 보일 수 있으므로 코드 블록의 문법처럼 구분해 줘.

Props에는 외부 계약만 담아. 호출자가 무엇을 넘기고 무엇을 기대할 수 있는지 표현하면 돼. 내부 상태와 ref, 파생 값까지 밖으로 드러내지 않으면 리팩터링 범위가 컴포넌트 안에 머물러.

Code

네 패턴 나란히·tsx
// 1. Inline
function Hello1({ name }: { name: string }) {
  return <p>Hello, {name}</p>;
}

// 2. 이름 붙인 type alias (일꾼)
type Hello2Props = {
  name: string;
  count?: number; // optional
};
export function Hello2({ name, count = 0 }: Hello2Props) {
  return <p>Hello, {name} (×{count})</p>;
}

// 3. Interface (declaration-merging 친화)
export interface Hello3Props {
  name: string;
}
export function Hello3({ name }: Hello3Props) {
  return <p>Hello, {name}</p>;
}

// 4. 추론 — 기존 컴포넌트 wrap
export function withSilly<P>(Inner: React.ComponentType<P>) {
  return function Sillied(props: P) {
    return <Inner {...props} />;
  };
}
네이티브 button prop을 내부 element에 전달하기·tsx
type ButtonProps = React.ComponentPropsWithoutRef<"button"> & {
  variant?: "primary" | "ghost";
};

export function Button({ variant = "primary", className, ...rest }: ButtonProps) {
  const base = "px-4 py-2 rounded-lg font-medium transition-colors";
  const tones = {
    primary: "bg-brand text-bg hover:bg-brand-strong",
    ghost: "bg-transparent text-fg hover:bg-bg-elevated",
  };
  return (
    <button
      className={`${base} ${tones[variant]} ${className ?? ""}`}
      {...rest}
    />
  );
}

// 호출자는 네이티브 button prop을 다시 선언하지 않고 그대로 넘길 수 있어.
// <Button onClick={...} aria-label="Save" disabled type="submit" />
제네릭 컴포넌트: 추론된 item 타입·tsx
// .tsx 에서 <T,>의 trailing comma가 JSX 와의 모호함 해소.
type ListProps<T> = {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  empty?: React.ReactNode;
};

export function List<T>({ items, renderItem, empty }: ListProps<T>) {
  if (items.length === 0) return <>{empty}</>;
  return (
    <ul>
      {items.map((item, i) => (
        <li key={i}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// 사용 — T가 { id: string; name: string }로 추론
// <List items={users} renderItem={(u) => u.name} />

External links

Exercise

React.ComponentPropsWithoutRef<'input'>을 확장하고 label과 선택적 error를 더한 Input을 만들어. 내부 label이 input을 감싸고 오류가 있을 때만 설명 문구를 보여 주게 해. 일반 text input, value와 onChange가 있는 제어 input, email과 aria-label을 쓰는 세 호출을 작성해. 네이티브 prop을 다시 나열하지 않아도 모두 컴파일되고, 필수 label 누락과 잘못된 prop 타입은 거부되는지 확인해.
Hint
label, error, className을 먼저 구조 분해하고 나머지 prop을 실제 input에 펼쳐. 직접 만든 prop이 네이티브 prop과 충돌하면 Omit으로 우선순위를 분명히 해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.