⚛️ 24. TypeScript × React 고급 패턴: 타입 안전한 컴포넌트 설계

📋 개요

제네릭 컴포넌트로 any 지옥 탈출, Discriminated Union으로 불가능한 props 조합 차단, 다형성 컴포넌트의 완전한 타입 안전성까지. 실무 TypeScript 패턴을 코드로 체득합니다.

📋 목차


📌 이 문서를 읽기 전에

⏱️ 예상 읽기 시간: 18분 (전체) / 핵심 파트만: 10분

🗺️ 이 문서의 흐름
[any 지옥 인식] → [제네릭 컴포넌트] → [Discriminated Union] → [조건부 Props] → [forwardRef] → [다형성 타입]

🎯 이 문서를 다 읽으면 할 수 있는 것

  • any 없이 타입 안전한 제네릭 리스트/셀렉트 컴포넌트를 설계할 수 있다
  • variant 에 따라 다른 props 를 요구하는 컴포넌트를 Discriminated Union 으로 설계할 수 있다
  • forwardRef 에 올바른 타입을 지정할 수 있다

🗺️ 이 문서의 배경 세계관: '영수네 커뮤니티'

  • 영철(신입): "공통 <Select> 컴포넌트 만들었는데요, options prop을 any[] 로 받아서... TypeScript 쓰는 의미가 없어진 것 같고, 버튼 컴포넌트에 href, icon, onClick 을 다 optional로 받다 보니 잘못된 조합도 타입 에러 없이 통과해버려요."
  • 영호(리드): "영철 님, any 는 TypeScript 보험을 파기하는 거예요. 제네릭과 Discriminated Union 으로 컴파일 타임에 모든 잘못된 사용을 잡아낼 수 있어요."

🤔 왜 알아야 하는가: any 지옥과 props 러시안 룰렛

🎯 이 섹션을 읽고 나면:

  • any 남용이 왜 TypeScript를 무력화하는지 설명할 수 있다
  • optional props 남발이 왜 런타임 버그로 이어지는지 이해한다

any 지옥 예시:

// ❌ any로 막아버린 Select 컴포넌트
interface SelectProps {
  options: any[];              // ← any: 타입 정보 사라짐
  onSelect: (v: any) => void; // ← any: 콜백 파라미터 타입 추론 불가
  getLabel?: (option: any) => string;
}
 
function Select({ options, onSelect }: SelectProps) {
  return (
    <ul>
      {options.map((option, i) => (
        <li key={i} onClick={() => onSelect(option)}>
          {option.name} {/* option이 any라 .name이 있는지 TypeScript가 모름 */}
        </li>
      ))}
    </ul>
  );
}
 
// 사용: 타입 에러 없이 모든 게 통과됨
<Select options={[1, 2, 3]} onSelect={v => console.log(v.name)} />
// → 런타임 에러: number에는 .name이 없음! 하지만 TypeScript는 경고 없음

optional props 러시안 룰렛:

// ❌ 모든 props를 optional로 → 잘못된 조합이 타입 에러 없이 통과
interface ButtonProps {
  variant?: 'default' | 'link' | 'icon';
  href?: string;    // link variant에서만 필요한데 optional
  icon?: ReactNode; // icon variant에서만 필요한데 optional
  onClick?: () => void;
}
 
// 타입 에러 없이 통과하는 잘못된 조합들:
<Button variant="link" />            // href 없는 링크 버튼 → 런타임 에러
<Button variant="icon" href="/home" /> // icon 없는 아이콘 버튼 → 빈 버튼
<Button variant="default" href="/home" onClick={() => {}} /> // href + onClick 동시 → 의미 불명

💡 한 줄로 기억하기
"any 는 TypeScript 보험증권의 면책 조항이야. any 가 있는 순간 그 타입 경로는 TypeScript의 보호를 받지 못해."


🏗️ 비유로 먼저 이해하기

🧒 5살에게 설명한다면?

레고 세트가 있다고 해봐. 영철이 방식은 "어떤 블록이든 어디든 끼울 수 있는 만능 구멍"(any)을 쓰는 거야. 모양이 안 맞아도 억지로 끼울 수 있어. 나중에 탑이 무너지면 어디서 잘못됐는지 찾기 엄청 어려워.

TypeScript 제네릭은 "이 구멍은 파란 블록만, 저 구멍은 빨간 블록만" 처럼 형태가 정확히 맞아야 끼울 수 있는 특수 구멍이야. 모양이 맞지 않으면 끼우는 순간 바로 "이 블록은 여기 안 들어가!" 라고 알려줘.


🧩 패턴 1: 제네릭 컴포넌트 🟡

🎯 이 섹션을 읽고 나면:

  • any 없이 타입 안전한 리스트/셀렉트 컴포넌트를 만들 수 있다
  • 제네릭 타입이 컴포넌트 props 전체에 흘러가는 원리를 이해한다

제네릭 Select 컴포넌트:

// ✅ 제네릭으로 타입 흐름 연결 — any 없이 완전한 타입 안전성
interface SelectProps<T> {
  options: T[];
  getLabel: (option: T) => string;   // T에서 표시할 텍스트 추출 방법
  getValue: (option: T) => string;   // T에서 유니크 키 추출 방법
  onSelect: (option: T) => void;     // 선택됐을 때 — T 타입으로 전달됨
  placeholder?: string;
}
 
// <T> 선언: 이 컴포넌트가 사용될 때 T가 결정됨
function Select<T>({ options, getLabel, getValue, onSelect, placeholder }: SelectProps<T>) {
  return (
    <ul>
      {placeholder && <li style={{ color: 'gray' }}>{placeholder}</li>}
      {options.map(option => (
        <li
          key={getValue(option)}         // getValue로 키 추출
          onClick={() => onSelect(option)} // onSelect에 T 타입 전달
        >
          {getLabel(option)}              // getLabel로 표시 텍스트 추출
        </li>
      ))}
    </ul>
  );
}

사용: T가 자동으로 추론됨:

interface User {
  id: string;
  name: string;
  email: string;
}
 
// T = User 로 자동 추론
<Select<User>
  options={users}
  getLabel={u => u.name}    // u: User 타입으로 자동 추론
  getValue={u => u.id}      // u: User 타입
  onSelect={user => {
    console.log(user.email); // user: User 타입 → .email 안전하게 접근
  }}
/>
 
// T = string 으로 자동 추론
<Select<string>
  options={['영철', '영호', '영숙']}
  getLabel={s => s}          // s: string
  getValue={s => s}
  onSelect={s => console.log(s.toUpperCase())} // string 메서드 안전하게 사용
/>

제네릭 List 컴포넌트 (renderItem 패턴):

// ✅ 렌더링 방식을 외부에서 주입받는 제네릭 리스트
interface ListProps<T> {
  items: T[];
  keyExtractor: (item: T) => string;
  renderItem: (item: T, index: number) => React.ReactNode;
  emptyMessage?: string;
}
 
function List<T>({ items, keyExtractor, renderItem, emptyMessage = '항목이 없어요.' }: ListProps<T>) {
  if (items.length === 0) return <p>{emptyMessage}</p>;
 
  return (
    <ul>
      {items.map((item, index) => (
        <li key={keyExtractor(item)}>
          {renderItem(item, index)} {/* 렌더링은 사용하는 쪽이 결정 */}
        </li>
      ))}
    </ul>
  );
}
 
// 사용: Post 타입으로 추론, renderItem 내부에서 post.title 안전하게 접근
<List<Post>
  items={posts}
  keyExtractor={post => post.id}
  renderItem={post => <PostCard title={post.title} author={post.author} />}
  emptyMessage="게시글이 없어요."
/>

💡 한 줄로 기억하기
"제네릭 컴포넌트는 '타입을 나중에 결정하는 틀'이야. any 대신 <T> 를 쓰면 props 전체에 타입이 흘러 컴파일 타임에 오류를 잡아낼 수 있어."


🎭 패턴 2: Discriminated Union Props 🟡

🎯 이 섹션을 읽고 나면:

  • variant 에 따라 필수 props 가 달라지는 컴포넌트를 타입 안전하게 설계할 수 있다
  • 잘못된 props 조합을 컴파일 타임에 차단할 수 있다

Discriminated Union(판별 유니온)type 필드(판별자)로 어떤 타입인지 결정하는 패턴이에요.

// ✅ Discriminated Union으로 variant별 props 타입 분리
type ButtonProps =
  | {
      variant: 'default';
      children: React.ReactNode;
      onClick?: () => void;
      // href, icon 없음 — 이 variant에서는 불필요
    }
  | {
      variant: 'link';
      children: React.ReactNode;
      href: string;          // link variant에서 href는 필수!
      target?: '_blank' | '_self';
    }
  | {
      variant: 'icon';
      icon: React.ReactNode; // icon variant에서 icon은 필수!
      label: string;         // 접근성: 스크린 리더용 레이블 필수
    };
 
function Button(props: ButtonProps) {
  // TypeScript가 각 case에서 props의 타입을 정확히 알고 있음
  switch (props.variant) {
    case 'default':
      return (
        <button onClick={props.onClick}>
          {props.children}
          {/* props.href 접근 → 타입 에러: 'default' variant에는 href 없음 */}
        </button>
      );
    case 'link':
      return (
        <a href={props.href} target={props.target}>
          {props.children}
          {/* props.href는 반드시 존재 — TypeScript가 보장 */}
        </a>
      );
    case 'icon':
      return (
        <button aria-label={props.label}>
          {props.icon}
        </button>
      );
  }
}

컴파일 타임 오류 잡기:

// ✅ 올바른 사용
<Button variant="default" onClick={() => {}}>저장</Button>
<Button variant="link" href="/profile">프로필</Button>
<Button variant="icon" icon={<HeartIcon />} label="좋아요" />
 
// ❌ 컴파일 타임 에러 — 런타임 전에 잡힘!
<Button variant="link" />                    // Error: href 없음
<Button variant="link" href="/home" icon={} /> // Error: link에는 icon 없음
<Button variant="icon" />                    // Error: icon, label 없음

Alert 컴포넌트 적용 예시:

type AlertProps =
  | { type: 'info'; message: string }
  | { type: 'success'; message: string; onDismiss?: () => void }
  | { type: 'error'; message: string; onRetry: () => void }; // error엔 onRetry 필수
 
function Alert(props: AlertProps) {
  const icons = { info: 'ℹ️', success: '✅', error: '❌' };
 
  return (
    <div className={`alert alert-${props.type}`}>
      <span>{icons[props.type]}</span>
      <p>{props.message}</p>
      {props.type === 'success' && props.onDismiss && (
        <button onClick={props.onDismiss}>닫기</button>
      )}
      {props.type === 'error' && (
        <button onClick={props.onRetry}>다시 시도</button>
        // props.onRetry: error variant에서 반드시 존재 — TypeScript 보장
      )}
    </div>
  );
}

💡 한 줄로 기억하기
"Discriminated Union 은 variant 별로 'props 계약서'를 따로 쓰는 거야. TypeScript가 각 계약서를 엄격하게 검사해서 잘못된 조합을 컴파일 전에 막아줘."


🔀 패턴 3: 조건부 Props 🔴

🎯 이 섹션을 읽고 나면:

  • "A가 있으면 B도 반드시 있어야 한다" 패턴을 타입으로 표현할 수 있다

"label이 있으면 htmlFor도 반드시 있어야 해" 패턴:

// ❌ 둘 다 optional → 잘못된 조합도 통과
interface TextFieldProps {
  label?: string;
  htmlFor?: string; // label 있으면 htmlFor도 있어야 하는데 강제 불가
}
 
// ✅ 조건부 Props: 둘 다 있거나, 둘 다 없거나
type TextFieldProps =
  | { label: string; htmlFor: string } // 둘 다 있음
  | { label?: never; htmlFor?: never }; // 둘 다 없음 (never로 상호 배제)
 
// 사용
<TextField label="이메일" htmlFor="email-input" /> // ✅
<TextField />                                       // ✅ (둘 다 없음)
<TextField label="이메일" />                        // ❌ htmlFor 없음 → 에러
<TextField htmlFor="email-input" />                 // ❌ label 없음 → 에러

XOR 타입 (A 또는 B, 둘 다는 불가):

// "onClick 또는 href 중 하나만" 패턴
type ExclusiveProps =
  | { onClick: () => void; href?: never }  // onClick만
  | { href: string; onClick?: never };     // href만
 
function ActionButton({ onClick, href, children }: ExclusiveProps & { children: React.ReactNode }) {
  if (href) return <a href={href}>{children}</a>;
  return <button onClick={onClick}>{children}</button>;
}
 
// 사용
<ActionButton onClick={() => {}}>클릭</ActionButton>  // ✅
<ActionButton href="/home">이동</ActionButton>         // ✅
<ActionButton onClick={() => {}} href="/home">?</ActionButton> // ❌ 둘 다 → 에러

💡 한 줄로 기억하기
"조건부 Props는 '이 prop이 있으면 저 prop도 반드시 있어야 해'를 타입으로 표현하는 것. never 키워드가 상호 배제의 핵심이야."


🧰 패턴 4: ComponentProps로 네이티브 props 상속 🟡

🎯 이 섹션을 읽고 나면:

  • 네이티브 HTML 요소의 모든 props 를 컴포넌트에 자동으로 상속할 수 있다
  • ComponentProps, ComponentPropsWithoutRef 의 차이를 이해한다

컴포넌트가 내부에서 <input> 을 렌더링한다면, placeholder, maxLength, type, disabled 등 input의 모든 속성을 직접 정의하지 않아도 상속받을 수 있어요.

import { ComponentProps } from 'react';
 
// ✅ input의 모든 네이티브 props를 상속
interface TextFieldProps extends ComponentProps<'input'> {
  label: string;   // 커스텀 prop 추가
  error?: string;  // 커스텀 prop 추가
}
 
function TextField({ label, error, ...inputProps }: TextFieldProps) {
  // ...inputProps 로 type, placeholder, disabled, maxLength 등 모두 전달
  return (
    <div>
      <label>{label}</label>
      <input
        {...inputProps}  // 네이티브 input props 전부 전달
        className={`text-field ${error ? 'error' : ''}`}
      />
      {error && <span className="error-msg">{error}</span>}
    </div>
  );
}
 
// 사용: 네이티브 input 속성 모두 자동완성 지원!
<TextField
  label="이메일"
  type="email"          // input 네이티브 속성
  placeholder="example@email.com" // input 네이티브 속성
  maxLength={100}       // input 네이티브 속성
  required              // input 네이티브 속성
  error="유효한 이메일을 입력해주세요"
/>

ComponentProps vs ComponentPropsWithoutRef:

// ComponentProps: ref 포함 (함수 컴포넌트에서는 안 씀)
// ComponentPropsWithoutRef: ref 제외 (일반적인 경우 이걸 써)
// ComponentPropsWithRef: ref 포함 (forwardRef 컴포넌트에 씀)
 
interface ButtonProps extends ComponentPropsWithoutRef<'button'> {
  loading?: boolean;
}
// → button의 모든 props (onClick, type, disabled...) 상속 + loading 추가

💡 한 줄로 기억하기
"extends ComponentProps<'input'> 은 HTML input의 수십 가지 속성을 한 번에 상속받는 마법이야. 커스텀 컴포넌트가 네이티브처럼 동작하게 만들어줘."


🎯 패턴 5: forwardRef 타입 지정 🟡

🎯 이 섹션을 읽고 나면:

  • forwardRef 에 올바른 타입을 지정할 수 있다
  • 부모 컴포넌트에서 자식 DOM 요소를 ref 로 직접 제어할 수 있다

forwardRef 가 필요한 상황:

// ❌ ref가 전달 안 되는 일반 컴포넌트
function TextInput(props: TextFieldProps) {
  return <input {...props} />;
}
 
// 부모에서 ref로 포커스 제어 시도 → 동작 안 함
const inputRef = useRef<HTMLInputElement>(null);
<TextInput ref={inputRef} /> // ❌ Warning: ref를 받지 못함

forwardRef 로 ref 전달:

// ✅ forwardRef<DOM 타입, Props 타입>
const TextInput = forwardRef<HTMLInputElement, TextFieldProps>(
  ({ label, error, ...props }, ref) => { // 두 번째 인자로 ref 받음
    return (
      <div>
        <label>{label}</label>
        <input
          ref={ref}    // ref를 실제 input DOM에 연결
          {...props}
        />
        {error && <span>{error}</span>}
      </div>
    );
  }
);
 
// DevTools에서 컴포넌트 이름 표시를 위해 displayName 설정
TextInput.displayName = 'TextInput';
 
// 사용: 부모에서 input DOM에 직접 접근 가능
function LoginForm() {
  const emailRef = useRef<HTMLInputElement>(null);
 
  const handleSubmit = () => {
    emailRef.current?.focus(); // input DOM에 직접 포커스!
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <TextInput ref={emailRef} label="이메일" type="email" />
    </form>
  );
}

💡 한 줄로 기억하기
"forwardRef<DOM타입, Props타입> — 제네릭 첫 번째는 ref가 연결되는 DOM 요소 타입, 두 번째는 props 타입이야. displayName 설정을 잊지 마."


🦎 패턴 6: 다형성 컴포넌트의 타입 안전성 🔴

🎯 이 섹션을 읽고 나면:

  • as prop으로 렌더링되는 HTML 태그를 변경하는 다형성 컴포넌트를 타입 안전하게 설계할 수 있다

20번 문서 에서 as prop 패턴을 배웠어요. 이번엔 TypeScript 타입까지 완전하게 만들어볼게요.

import { ElementType, ComponentPropsWithoutRef } from 'react';
 
// ✅ 다형성 컴포넌트 타입 헬퍼
// C: 렌더링할 HTML 태그 또는 컴포넌트 타입
// OwnProps: 컴포넌트 고유 props
type PolymorphicProps<C extends ElementType, OwnProps = {}> = OwnProps & {
  as?: C; // 어떤 태그로 렌더링할지
} & Omit<
  ComponentPropsWithoutRef<C>, // C 태그의 모든 네이티브 props
  keyof OwnProps | 'as'        // 중복 제거
>;
 
// 실제 사용 예: Button 컴포넌트
type ButtonOwnProps = {
  variant?: 'primary' | 'secondary' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
};
 
function Button<C extends ElementType = 'button'>({
  as,
  variant = 'primary',
  size = 'md',
  children,
  ...rest
}: PolymorphicProps<C, ButtonOwnProps> & { children?: React.ReactNode }) {
  const Component = as || 'button';
  return (
    <Component
      className={`btn btn-${variant} btn-${size}`}
      {...rest}
    >
      {children}
    </Component>
  );
}

타입 안전성 확인:

// ✅ button 태그로 렌더링 — button 속성 자동완성
<Button onClick={() => {}}>클릭</Button>
<Button type="submit" disabled>제출</Button>
 
// ✅ a 태그로 렌더링 — a 태그 속성이 자동완성됨
<Button as="a" href="/profile" target="_blank">프로필로 이동</Button>
 
// ✅ Next.js Link로 렌더링
<Button as={Link} href="/dashboard">대시보드</Button>
 
// ❌ 타입 에러: button 태그에 href는 없음
<Button href="/home">클릭</Button>
 
// ❌ 타입 에러: a 태그에 type="submit"은 의미 없음 (경고)
<Button as="a" type="submit">?</Button>

💡 한 줄로 기억하기
"다형성 컴포넌트의 타입 안전성은 ElementType 제네릭 + ComponentPropsWithoutRef<C> 조합으로 만들어. as="a" 이면 a 태그 속성이, as="button" 이면 button 속성이 자동으로 추론돼."


💥 에러 해결 카탈로그


JSX element type 'Component' does not have any construct or call signatures

언제 나오는가?

function Button({ as: Component = 'button', ...props }) {
  return <Component {...props} />; // 💥 타입 에러
}

원인: as prop의 타입이 제대로 지정되지 않아서 TypeScript가 Component 를 JSX 요소로 렌더링할 수 있는지 모름.

해결책:

function Button<C extends ElementType = 'button'>({
  as,
  ...props
}: { as?: C } & ComponentPropsWithoutRef<C>) {
  const Component = as || 'button'; // ElementType으로 타입 확정
  return <Component {...props} />;
}

Type 'string' is not assignable to type 'never'

언제 나오는가?

type Props = { href: string; onClick?: never } | { onClick: () => void; href?: never };
<Button href="/home" onClick={() => {}} /> // Error: Type 'string' is not assignable to 'never'

원인: XOR 타입에서 두 prop을 동시에 넘길 수 없어요 — 의도된 오류!

해설: 이 오류는 버그를 잡아낸 거예요. hrefonClick 을 동시에 넘기면 안 되는 설계를 TypeScript가 컴파일 타임에 막아준 것이에요.


❌ 제네릭 컴포넌트가 TypeScript에서 <T> 를 HTML 태그로 인식

언제 나오는가? .tsx 파일에서:

// ❌ TSX에서 <T>를 HTML 태그로 파싱할 수 있음
function Select<T>(props: SelectProps<T>) { ... } // 파싱 오류 가능

해결책:

// 제약 조건 추가로 TSX 파서에게 이게 제네릭임을 알림
function Select<T extends object>(props: SelectProps<T>) { ... }
// 또는 trailing comma
function Select<T,>(props: SelectProps<T>) { ... }

🏁 이번에 배운 내용 총정리

📋 TypeScript 패턴 6종 요약

패턴문제해결
제네릭 컴포넌트any[] 로 타입 소실<T> 제네릭으로 타입 흐름 유지
Discriminated Unionoptional props 조합 버그variant 별 독립 타입 계약
조건부 PropsA 없이 B만 쓰는 버그never 로 상호 배제 강제
ComponentProps네이티브 props 직접 나열extends ComponentProps<'input'>
forwardRefref 전달 불가forwardRef<DOM, Props>
다형성 타입as prop 타입 불안전ElementType + ComponentPropsWithoutRef<C>

⚠️ 절대 하지 말 것

❌ 나쁜 예✅ 좋은 예이유
options: any[]options: T[]any는 타입 보호 파기
모든 props optionalDiscriminated Union잘못된 조합 런타임 버그
@ts-ignore 남발올바른 타입 정의문제를 숨기는 것
forwardRef 없이 ref 전달forwardRef<DOM, Props>ref 미작동

🐣 영철이의 퇴근 일기

TypeScript를 쓰면서도 any를 도배하거나 전부 ?(Optional) 처리해놓고 '왜 자동완성이 안 되지' 하며 답답해하던 내 모습이 부끄러워졌다.

💡 "TypeScript는 옵션이 아니라 계약서다. 제네릭과 Discriminated Union으로 엉터리 계약(잘못된 Props 조합)은 런타임이 오기 전에, 에디터 단에서 찢어버려라."

any 없이 짠 컴포넌트가 마치 꽉 물려 돌아가는 톱니바퀴처럼 완벽하게 타입을 뱉어낼 때의 쾌감이란! 특히 다형성 컴포넌트에서 as 태그 속성에 따라 네이티브 Props가 휙휙 바뀌는 걸 보니 진짜 시니어의 코드를 보고 있다는 실감이 난다.


📝 마무리 퀴즈

Q1. 아래 Discriminated Union 타입에서 컴파일 에러가 나는 사용은?

type CardProps =
  | { variant: 'post'; title: string; content: string }
  | { variant: 'user'; name: string; avatar: string }
  | { variant: 'ad'; imageUrl: string; link: string };
  • A) <Card variant="post" title="제목" content="내용" />
  • B) <Card variant="user" name="영철" avatar="/img.png" />
  • C) <Card variant="post" name="영철" />
  • D) <Card variant="ad" imageUrl="/ad.png" link="/offer" />

정답: C

variant="post" 일 때는 titlecontent 가 필요한데, name 만 있고 title, content 가 없어요. TypeScript가 컴파일 에러를 발생시켜요.

📌 핵심 기억법: "Discriminated Union 은 variant 별 계약서야. variant="post" 를 선택했으면 post 계약서를 따라야 해."


Q2. 아래 빈칸을 채워보자.

// input의 모든 네이티브 props를 상속하는 TextField
interface TextFieldProps extends ________<'input'> {
  label: string;
  error?: string;
}
 
// ref를 전달받는 TextInput 컴포넌트
const TextInput = ________<HTMLInputElement, TextFieldProps>(
  ({ label, ...props }, ref) => <input ref={ref} {...props} />
);

정답: 1번: ComponentProps (또는 ComponentPropsWithoutRef), 2번: forwardRef

📌 핵심 기억법: "ComponentProps 는 네이티브 props 상속, forwardRef 는 ref 전달. 둘 다 '컴포넌트를 네이티브처럼 동작하게' 만드는 패턴이야."


Q3. 친구에게 설명한다면?

제네릭 컴포넌트가 any 보다 나은 이유를 예시를 들어 설명해봐.

예시 답변:

"any 로 만든 <Select> 는 숫자 배열을 넣어도, 문자열 배열을 넣어도 타입 에러가 없어. onSelect 콜백에서 받는 값도 any.name 을 써도 TypeScript가 모른 척해. 근데 실제로 숫자에 .name 은 없으니까 런타임에서 undefined 가 나와. 제네릭 <Select<User>>optionsUser[] 를 넣으면 onSelect 콜백의 파라미터가 자동으로 User 타입이 돼. .name, .email 같은 User 속성을 쓰면 TypeScript가 OK, 없는 속성을 쓰면 컴파일 에러. 버그를 코드 작성 순간에 잡을 수 있어."


🔗 더 알아보기