제네릭 컴포넌트 타입 — 재사용 가능한 <List<T>> 만들기
List, Grid, Select 같은 재사용 컴포넌트에서 item 타입 T가 뒤로 새지 않게 만드는 패턴. forwardRef 캐스팅, keyExtractor 보조 함수, discriminated prop까지.
React에서 <List items={users} renderItem={...}> 같은 재사용 컴포넌트를 만들 때, T가 뒤로 새지 않게 만드는 게 의외로 까다롭습니다. any[]로 두면 prop 추론이 풀리고, unknown[]으로 두면 호출 측에서 매번 좁혀야 합니다. 제네릭 컴포넌트는 이 둘을 모두 피하면서 호출 측 추론을 보존합니다.
1. 단순한 제네릭 함수 컴포넌트
1type ListProps<T> = { 2 items: readonly T[]; 3 renderItem: (item: T, index: number) => React.ReactNode; 4}; 5 6function List<T>({ items, renderItem }: ListProps<T>) { 7 return <ul>{items.map((it, i) => <li key={i}>{renderItem(it, i)}</li>)}</ul>; 8}
호출 시 T가 자동 추론됩니다.
1<List 2 items={users} // User[] 3 renderItem={(u) => <span>{u.name}</span>} // u: User 4/>
renderItem 안에서 u가 User로 좁혀져 자동완성도 됩니다.
2. forwardRef와 함께
forwardRef는 제네릭과 약간 까다롭게 결합됩니다. 가장 깔끔한 방법은 type assertion으로 시그니처를 다시 적어주는 것입니다.
1function ListInner<T>( 2 props: ListProps<T>, 3 ref: React.Ref<HTMLUListElement>, 4) { 5 return <ul ref={ref}>{/* ... */}</ul>; 6} 7 8const List = React.forwardRef(ListInner) as <T>( 9 props: ListProps<T> & { ref?: React.Ref<HTMLUListElement> } 10) => React.ReactElement;
또는 React 19부터는 forwardRef 자체가 필요 없어지면서 한결 단순해집니다.
1function List<T>({ items, renderItem, ref }: ListProps<T> & { ref?: React.Ref<HTMLUListElement> }) { 2 return <ul ref={ref}>{/* ... */}</ul>; 3}
3. 조건부 prop으로 안전성 더하기
keyExtractor를 받는 경우 — 키 함수가 반환하는 값이 string|number여야 합니다.
1type ListProps<T, K extends string | number> = { 2 items: readonly T[]; 3 keyExtractor: (item: T) => K; 4 renderItem: (item: T) => React.ReactNode; 5}; 6 7function List<T, K extends string | number>({ 8 items, keyExtractor, renderItem, 9}: ListProps<T, K>) { 10 return ( 11 <ul> 12 {items.map((it) => ( 13 <li key={keyExtractor(it)}>{renderItem(it)}</li> 14 ))} 15 </ul> 16 ); 17}
keyExtractor 반환이 boolean이면 컴파일 에러가 납니다.
4. Discriminated prop으로 모드 전환
같은 컴포넌트가 두 모드를 지원할 때 — 빈 상태 컴포넌트가 있을 때만 emptyMessage 받는 식.
1type ListProps<T> = 2 | { items: readonly T[]; renderItem: (item: T) => React.ReactNode; empty?: undefined } 3 | { items: readonly T[]; renderItem: (item: T) => React.ReactNode; empty: React.ReactNode }; 4 5function List<T>(props: ListProps<T>) { 6 if (props.items.length === 0 && props.empty) return <>{props.empty}</>; 7 return <ul>{props.items.map((it, i) => <li key={i}>{props.renderItem(it)}</li>)}</ul>; 8}
5. 주의할 점
- 화살표 함수로 정의하면 JSX와 충돌할 수 있습니다.
const List = <T,>(...)처럼 trailing comma를 써야 파서가 제네릭으로 인식합니다 (.tsx). T를 너무 많이 노출하면 호출 측에서 매번 명시적으로 적어야 할 수 있습니다. 추론으로 풀리는 형태를 우선 설계합니다.- 매우 복잡한 제네릭은 가독성이 떨어집니다. 4단계 이상 중첩되면 별도 타입으로 분리합니다.
6. 정리
| 패턴 | 사용 시점 |
|---|---|
<T> 단순 제네릭 | 일반 list/grid/select 컴포넌트 |
forwardRef + 캐스팅 | ref 필요한 list |
<T, K> 조합 | keyExtractor 같은 보조 함수 안전화 |
| Discriminated prop | 모드별로 prop이 달라질 때 |
any[]나 unknown[]을 쓰지 않고, 호출 측 추론을 그대로 살리는 게 핵심입니다. 한 번 만들어 두면 전 프로젝트에서 안전한 list 컴포넌트가 자리잡습니다.
참고: React TypeScript Cheatsheet — Generic Components.