⚛️ 24. TypeScript × React 고급 패턴: 타입 안전한 컴포넌트 설계
📋 개요
제네릭 컴포넌트로 any 지옥 탈출, Discriminated Union으로 불가능한 props 조합 차단, 다형성 컴포넌트의 완전한 타입 안전성까지. 실무 TypeScript 패턴을 코드로 체득합니다.
📋 목차
- 이 문서를 읽기 전에
- 왜 알아야 하는가: any 지옥과 props 러시안 룰렛
- 비유로 먼저 이해하기
- 패턴 1: 제네릭 컴포넌트
- 패턴 2: Discriminated Union Props
- 패턴 3: 조건부 Props
- 패턴 4: ComponentProps로 네이티브 props 상속
- 패턴 5: forwardRef 타입 지정
- 패턴 6: 다형성 컴포넌트의 타입 안전성
- 에러 해결 카탈로그
- 이번에 배운 내용 총정리
- 마무리 퀴즈
- 더 알아보기
📌 이 문서를 읽기 전에
⏱️ 예상 읽기 시간: 18분 (전체) / 핵심 파트만: 10분
🗺️ 이 문서의 흐름
[any 지옥 인식] → [제네릭 컴포넌트] → [Discriminated Union] → [조건부 Props] → [forwardRef] → [다형성 타입]
🎯 이 문서를 다 읽으면 할 수 있는 것
-
any없이 타입 안전한 제네릭 리스트/셀렉트 컴포넌트를 설계할 수 있다 - variant 에 따라 다른 props 를 요구하는 컴포넌트를 Discriminated Union 으로 설계할 수 있다
-
forwardRef에 올바른 타입을 지정할 수 있다
🗺️ 이 문서의 배경 세계관: '영수네 커뮤니티'
- 영철(신입): "공통
<Select>컴포넌트 만들었는데요,optionsprop을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: 다형성 컴포넌트의 타입 안전성 🔴
🎯 이 섹션을 읽고 나면:
asprop으로 렌더링되는 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을 동시에 넘길 수 없어요 — 의도된 오류!
해설: 이 오류는 버그를 잡아낸 거예요. href 와 onClick 을 동시에 넘기면 안 되는 설계를 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 Union | optional props 조합 버그 | variant 별 독립 타입 계약 |
| 조건부 Props | A 없이 B만 쓰는 버그 | never 로 상호 배제 강제 |
| ComponentProps | 네이티브 props 직접 나열 | extends ComponentProps<'input'> |
| forwardRef | ref 전달 불가 | forwardRef<DOM, Props> |
| 다형성 타입 | as prop 타입 불안전 | ElementType + ComponentPropsWithoutRef<C> |
⚠️ 절대 하지 말 것
| ❌ 나쁜 예 | ✅ 좋은 예 | 이유 |
|---|---|---|
options: any[] | options: T[] | any는 타입 보호 파기 |
| 모든 props optional | Discriminated 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"일 때는title과content가 필요한데,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>>는options에User[]를 넣으면onSelect콜백의 파라미터가 자동으로User타입이 돼..name,
🔗 더 알아보기
- React TypeScript Cheatsheet — 실무 패턴 총망라
- TypeScript Handbook — Generics
- 20번 문서 — 다형성과 접근성 —
asprop 기본 패턴 - Matt Pocock의 TypeScript 강좌 — 실무 TypeScript 패턴