TypeScript 제네릭 완전 정복
·8분 읽기
TypeScript를 쓰다 보면 제네릭(Generics)을 피해갈 수 없습니다. 처음에는 <T>가 낯설게 느껴지지만, 제네릭을 이해하면 타입 안전성을 유지하면서도 재사용 가능한 코드를 작성할 수 있습니다.
제네릭이 필요한 이유#
제네릭 없이 다양한 타입을 처리하려면 어떻게 해야 할까요?
// any를 쓰면 타입 안전성을 잃습니다
function identity(arg: any): any {
return arg
}
const result = identity(42)
result.toUpperCase() // 런타임 에러! TypeScript가 잡아주지 못함
제네릭을 사용하면 타입 안전성을 유지할 수 있습니다.
function identity<T>(arg: T): T {
return arg
}
const num = identity(42) // T는 number로 추론
const str = identity('hello') // T는 string으로 추론
num.toUpperCase() // ✅ TypeScript가 오류를 잡아줍니다
str.toUpperCase() // ✅ 정상 동작
기본 문법#
// 함수 제네릭
function firstItem<T>(arr: T[]): T | undefined {
return arr[0]
}
// 화살표 함수 제네릭 (TSX 파일에서는 <T,> 로 작성)
const firstItem = <T>(arr: T[]): T | undefined => arr[0]
// 클래스 제네릭
class Stack<T> {
private items: T[] = []
push(item: T): void {
this.items.push(item)
}
pop(): T | undefined {
return this.items.pop()
}
}
const stack = new Stack<number>()
stack.push(1)
stack.push(2)
stack.pop() // 2
제약 조건 (Constraints)#
extends를 사용해 제네릭에 제약을 걸 수 있습니다.
// T는 length 프로퍼티를 가져야 함
function getLength<T extends { length: number }>(arg: T): number {
return arg.length
}
getLength('hello') // ✅ 문자열은 length가 있음
getLength([1, 2, 3]) // ✅ 배열은 length가 있음
getLength(42) // ❌ 숫자는 length가 없음
// 다른 타입 파라미터로 제약
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { name: '홍길동', age: 30 }
getProperty(user, 'name') // ✅ '홍길동'
getProperty(user, 'email') // ❌ user에 email 프로퍼티 없음
여러 타입 파라미터#
function zip<T, U>(arr1: T[], arr2: U[]): [T, U][] {
return arr1.map((item, i) => [item, arr2[i]])
}
const result = zip([1, 2, 3], ['a', 'b', 'c'])
// [[1, 'a'], [2, 'b'], [3, 'c']]
실전 예제: API 응답 처리#
실무에서 가장 자주 쓰이는 패턴입니다.
interface ApiResponse<T> {
data: T
status: number
message: string
}
interface User {
id: number
name: string
email: string
}
interface Post {
id: number
title: string
content: string
}
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
const res = await fetch(url)
return res.json()
}
// 사용 시 타입이 자동으로 추론됨
const userResponse = await fetchData<User>('/api/users/1')
userResponse.data.name // ✅ string으로 타입 추론
const postResponse = await fetchData<Post>('/api/posts/1')
postResponse.data.title // ✅ string으로 타입 추론
내장 유틸리티 타입#
TypeScript는 제네릭을 활용한 유틸리티 타입을 제공합니다.
Partial / Required#
interface User {
id: number
name: string
email: string
}
// 모든 프로퍼티를 선택적으로
type PartialUser = Partial<User>
// { id?: number; name?: string; email?: string }
// 모든 프로퍼티를 필수로
type RequiredUser = Required<PartialUser>
// { id: number; name: string; email: string }
Pick / Omit#
// 특정 프로퍼티만 선택
type UserPreview = Pick<User, 'id' | 'name'>
// { id: number; name: string }
// 특정 프로퍼티를 제외
type UserWithoutId = Omit<User, 'id'>
// { name: string; email: string }
Record#
// 키-값 매핑 타입 생성
type UserMap = Record<string, User>
// { [key: string]: User }
const users: UserMap = {
'user-1': { id: 1, name: '홍길동', email: 'hong@example.com' },
'user-2': { id: 2, name: '김철수', email: 'kim@example.com' },
}
ReturnType / Parameters#
function createUser(name: string, age: number): User {
return { id: Math.random(), name, email: '' }
}
// 함수 반환 타입 추출
type CreatedUser = ReturnType<typeof createUser> // User
// 함수 파라미터 타입 추출
type CreateUserParams = Parameters<typeof createUser> // [string, number]
유틸리티 타입 비교#
| 유틸리티 타입 | 설명 | 예시 |
|---|---|---|
Partial<T> | 모든 프로퍼티를 선택적으로 | 업데이트 요청 DTO |
Required<T> | 모든 프로퍼티를 필수로 | 완전한 엔티티 |
Pick<T, K> | 특정 프로퍼티만 선택 | 목록 조회 응답 |
Omit<T, K> | 특정 프로퍼티를 제외 | 생성 요청 DTO (id 제외) |
Record<K, T> | 키-값 매핑 | 캐시, 맵 자료구조 |
Readonly<T> | 모든 프로퍼티를 읽기 전용으로 | 불변 설정값 |
마무리#
제네릭은 처음에 어렵게 느껴지지만, 핵심은 간단합니다. 타입을 변수처럼 다루는 것이죠. any 대신 제네릭을 사용하면 타입 안전성을 유지하면서 재사용 가능한 코드를 작성할 수 있습니다. 특히 API 응답 처리, 유틸리티 함수, 공통 컴포넌트에서 제네릭을 적극 활용해보세요.
관련 포스트
개발#성능 최적화#Web Vitals#Next.js#프론트엔드
웹 성능 최적화 실전 가이드: Core Web Vitals와 최적화 기법
LCP, FID, CLS 등 Core Web Vitals의 의미와 실제 프론트엔드 성능을 개선하는 실전 기법을 정리했습니다.
·9분 읽기
개발#Docker#DevOps#컨테이너#인프라
Docker 입문 가이드: 개발자가 꼭 알아야 할 컨테이너 기초
Docker가 왜 필요한지부터 이미지, 컨테이너, Docker Compose까지 개발자 관점에서 실용적으로 정리했습니다.
·8분 읽기
개발#API#REST#GraphQL#백엔드
REST API vs GraphQL: 실무에서 뭘 선택해야 할까?
REST와 GraphQL의 핵심 차이를 이해하고, 프로젝트 상황에 따른 올바른 선택 기준을 정리했습니다.
·8분 읽기