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
타입 잡힌 prop 은 컴포넌트 작성자와 모든 호출자 사이의 작은 조약. 컴파일러가 유일한 중립 당사자. 조약 지키면 IDE 가 두 번째 눈이 돼.

Prop 타입 잡는 네 가지 방식

  1. 인라인 object 타입 — 가장 빠름, 가장 로컬. 작은 컴포넌트에 좋아.
  2. 이름 붙인 type alias — export 가능, 재사용 가능, intersection (&) + union (|) 으로 조합 가능.
  3. 이름 붙인 interface — type 비슷, declaration merging 으로 확장 가능. 서드파티가 확장하길 기대하는 prop 모양에 사용.
  4. 함수 시그니처에서 추론 — 컴포넌트 감싸는 HOC 스타일 헬퍼용.

실전에선 이름 붙인 type alias 가 이김. 유연 (union, intersection, mapped type 다 동작) + declaration merging 으로 우연히 충돌하지 않음.

`children` prop

거의 모든 레이아웃 컴포넌트가 children 받음. 캐논 타입은 React.ReactNode — 문자열, 숫자, JSX, fragment, 배열, null, undefined, boolean 다 커버. 실제 이유 (예: '단일 문자열만 받음') 없으면 narrow 시도하지 마 — 정당한 사용을 막게 돼.

Native element 확장 패턴

흔한 모양: 본인 컴포넌트가 <button> 을 추가 동작과 함께 렌더, 호출자가 아무 네이티브 button prop (onClick, aria-label, disabled 등) 도 본인이 일일이 재선언 없이 넘길 수 있게 하고 싶음. TS 네이티브 방식: React.ComponentPropsWithoutRef<'button'> 확장.

컴포넌트의 제네릭

제네릭 컴포넌트가 타입 잡힌 <List items={users} renderItem={(u) => ...} /> 짓는 방법 — TypeScript 가 users 타입에서 uUser 임을 추론. 문법에 .tsx 파일 quirk 있음 (타입 파라미터 <T> 가 JSX 처럼 보임) — 코드 블록에 나와.

구현 말고 계약을 타입. Props 타입은 호출자가 뭘 넘겨야 하고 뭘 기대할 수 있는지 묘사. 그게 public face. 내부 state, ref, 파생 값 — props 타입에 안 들어감. 경계 깨끗하게 유지하면 리팩토링이 로컬에 머물러.

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} />;
  };
}
Native-element 확장 — 알 수 없는 prop 을 내부 button 에 forward·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}
    />
  );
}

// 호출자는 어떤 native 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'> 확장 + 두 prop 추가 (label: string, error?: string) 하는 타입 잡힌 Input 컴포넌트. <label> 이 input 감싸고 에러 있으면 그 밑에 빨간색으로 표시. Input 세 방식으로 사용: (1) 평범한 텍스트 input, (2) onChange + value, (3) type='email' + aria-label. TypeScript 가 모든 네이티브 input prop 을 본인이 나열 없이 받는지 확인.
Hint
Rest props 를 내부 <input> 에 spread. 본인이 처리하는 prop (label, error, 가능하면 className) destructure 후 ...rest.

Progress

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

댓글 0

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

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