TypeScript Generic 쉽게 이해하기 — 실무 예제로 보는 타입 재사용

TypeScript를 쓰다 보면, “이 타입을 반복해서 정의하기 귀찮다”는 생각 한 번쯤 하셨을 거예요.
예를 들어 함수나 컴포넌트가 여러 타입을 받을 수 있는데, 매번 새로운 타입을 만들기는 부담스럽죠.
이럴 때 등장하는 게 바로 **Generic (제네릭)**이에요.
한마디로, 타입에 변수를 주는 방법이에요.
코드를 유연하게 만들면서도 타입 안전성을 유지할 수 있는 TypeScript의 핵심 기능입니다.
💡 Generic이란?
Generic은 “타입을 변수처럼 다루는 문법”이에요.
즉, 함수나 클래스, 인터페이스에서 타입을 직접 넘겨서 재사용할 수 있게 해줍니다.
function identity<T>(value: T): T {
return value
}
const a = identity<string>('Hello')
const b = identity<number>(123)여기서 <T>가 바로 Generic 타입 변수입니다.
T는 호출할 때마다 바뀔 수 있는 “타입의 자리”라고 생각하시면 됩니다.
| 구분 | 의미 |
|---|---|
<T> |
Type 변수 선언 |
T |
전달받은 타입 |
identity<T>(value: T): T |
T 타입을 받아 T 타입으로 반환 |
즉, 함수가 어떤 타입을 받을지 몰라도,
그 타입이 전달되는 순간 안전하게 추론할 수 있게 되는 거예요.
🧩 Generic 기본 문법 3단계
1️⃣ Generic 함수
function wrap<T>(value: T) {
return { value }
}
const num = wrap(10) // { value: number }
const str = wrap('Hello') // { value: string }TypeScript는 호출 시점의 타입을 추론해 <T>에 자동으로 넣어줍니다.
2️⃣ Generic 인터페이스
interface ApiResponse<T> {
data: T
status: number
}
const userResponse: ApiResponse<{ name: string }> = {
data: { name: '시황' },
status: 200,
}3️⃣ Generic 클래스
class Storage<T> {
private items: T[] = []
add(item: T) {
this.items.push(item)
}
getAll(): T[] {
return this.items
}
}
const stringStore = new Storage<string>()
stringStore.add('Hello')
const numberStore = new Storage<number>()
numberStore.add(100)💬 이렇게 Generic을 쓰면 타입별로 따로 클래스를 만들 필요 없이,
하나의 코드로 모든 타입을 커버할 수 있습니다.
⚙️ 제약 조건 (extends)
Generic은 자유롭지만, 때로는 특정 구조만 받도록 제한할 수 있어요.
function getLength<T extends { length: number }>(value: T) {
return value.length
}
getLength('hello') // ✅ string OK
getLength([1, 2, 3]) // ✅ array OK
getLength(123) // ❌ number는 length 없음extends를 사용하면 T가 { length: number } 형태를 반드시 가져야 한다는 의미예요.
🧠 실무에서 자주 쓰는 Generic 패턴
1️⃣ API 응답 타입 정의
interface ApiResponse<T> {
success: boolean
data: T
}
function fetchData<T>(url: string): Promise<ApiResponse<T>> {
return fetch(url).then((res) => res.json())
}
// 사용자 정보
type User = { id: number; name: string }
fetchData<User>('/api/user').then((res) => {
if (res.success) {
console.log(res.data.name) // 타입 자동완성 ✅
}
})실무에서 API 호출 시 Generic을 사용하면,
매번 다른 데이터 구조를 받아도 타입 안전성을 유지할 수 있어요.
2️⃣ 유틸리티 함수에서의 타입 재사용
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 }
}
const result = merge({ name: '시황' }, { age: 29 })
// result: { name: string; age: number }서로 다른 객체를 합칠 때도, 두 타입을 자동으로 병합해줍니다.
3️⃣ React 컴포넌트에서도 Generic 활용
type ListProps<T> = {
items: T[]
renderItem: (item: T) => JSX.Element
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map(renderItem)}</ul>
}
const users = [{ id: 1, name: '시황' }, { id: 2, name: '병규' }]
<List
items={users}
renderItem={(user) => <li key={user.id}>{user.name}</li>}
/>React 컴포넌트에서도 Generic을 활용하면,
컴포넌트의 재사용성과 타입 안전성을 동시에 얻을 수 있어요.
🧩 keyof와 함께 쓰기
Generic은 keyof와 함께 쓰면 타입을 더욱 정교하게 조합할 수 있습니다.
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { name: '시황', age: 29 }
const name = getProperty(user, 'name') // string
const age = getProperty(user, 'age') // number
keyof는 객체의 키를 문자열 리터럴 타입으로 추출합니다.
이 조합은 ORM, 데이터 접근 함수, UI Form 관리 등 실무에서 자주 쓰여요.
🧠 Generic Default 타입
Generic에도 **기본값(default type)**을 줄 수 있습니다.
interface ApiResponse<T = unknown> {
data: T
status: number
}
const res: ApiResponse = {
data: 'ok', // T는 자동으로 unknown으로 추론
status: 200,
}Default 타입은 대부분 라이브러리 내부에서 “확장성 있는 타입 선언”을 위해 사용됩니다.
📊 정리
| 개념 | 설명 | 예시 |
|---|---|---|
| Generic | 타입을 변수처럼 사용하는 문법 | <T>(value: T) => T |
| extends | 타입 제약 조건 부여 | <T extends { length: number }> |
| keyof | 객체 키를 타입으로 추출 | <K extends keyof T> |
| Default Type | 기본 타입 지정 | <T = string> |
💬 마무리하며
Generic은 처음엔 어렵게 느껴지지만,
한 번 제대로 이해하면 TypeScript를 진짜 **‘타입 안전한 언어’**로 만들어주는 핵심이에요.
실무에서는 API 응답, React props, 유틸 함수, 데이터 모델 등에서 빠짐없이 등장합니다.
결국 Generic의 본질은 “유연하지만 안전한 코드”예요.
타입을 변수처럼 다루는 순간, 코드의 재사용성이 폭발적으로 늘어납니다.
#TypeScript #Generic #타입스크립트 #제네릭 #타입재사용 #TypeSafety #프론트엔드 #React #타입추론