제네릭 입문 — <T> 한 글자의 힘
제네릭은 "타입을 변수처럼 받는 함수". 라이브러리 코드의 80% 가 여기서 나옵니다.
이 한 줄을 본 적 있나요?
function identity<T>(value: T): T {
return value;
}
<T> 가 처음 보면 외계어 같지만, 알고 나면 "왜 진작 안 썼지" 싶어집니다. 제네릭은 "타입을 매개변수처럼 받는 함수/타입" 이에요. 6편은 제네릭의 첫 90% 를 정리합니다.
왜 제네릭이 필요한가
// 단순 버전 — any 사용 (타입 정보 사라짐)
function first(arr: any[]): any {
return arr[0];
}
const n = first([1, 2, 3]); // n 의 타입: any ← 망함
n.toUpperCase(); // 컴파일 통과 (런타임 에러)
// 제네릭 버전 — 타입 보존
function first<T>(arr: T[]): T {
return arr[0];
}
const n2 = first([1, 2, 3]); // n2: number
const s2 = first(["a", "b", "c"]); // s2: string
n2.toFixed(2); // OK
s2.toUpperCase(); // OK
핵심 한 줄. 제네릭은 함수가 "어떤 타입이든 받지만, 입력과 출력의 관계를 보존" 하게 해줍니다. any 는 관계를 끊지만, 제네릭은 살립니다.
호출할 때 — 추론이 알아서
function box<T>(value: T): { value: T } {
return { value };
}
// 명시 호출 (필요 없음)
box<string>("hi");
// 추론에 맡김 (권장)
box("hi"); // T = string 추론
box(42); // T = number 추론
box({a: 1}); // T = {a: number} 추론
대부분 추론이 알아서 합니다. 명시 호출이 필요한 경우는 빈 배열·null 같은 추론 부족 케이스, 또는 라이브러리 API 설명 등 명확성을 위해서입니다.
여러 개의 타입 매개변수
function pair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
pair("name", "준성"); // [string, string]
pair("age", 39); // [string, number]
pair(1, true); // [number, boolean]
관례적으로 한 글자 — T, U, V, K, V — 를 씁니다. 의미가 있으면 풀어써도 됩니다 — <TKey, TValue>.
extends 제약 — "이 타입 중 하나여야"
// 길이 속성이 있는 것만 받기
function logLength<T extends { length: number }>(value: T): T {
console.log(value.length);
return value;
}
logLength("hello"); // string 은 length 있음 → OK
logLength([1, 2, 3]); // 배열도 length 있음 → OK
logLength(42); // ❌ number 는 length 없음
// keyof 와 함께
function pick<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "준성", age: 39 };
pick(user, "name"); // string
pick(user, "age"); // number
pick(user, "phone"); // ❌ phone 은 user 키 아님
extends 는 "최소한 이 모양을 가져야" 라는 제약입니다. JS 의 클래스 상속과 글자만 같고 의미는 다릅니다.
기본값 — T = 기본 타입
// React 의 useState 와 비슷한 패턴
function createState<T = string>(initial?: T): [T | undefined, (v: T) => void] {
let value = initial;
const set = (v: T) => { value = v; };
return [value, set];
}
createState<number>(0); // T = number 명시
createState("hi"); // T = string 추론
createState(); // T = string 기본값 (initial 없으니까 추론 못함)
제네릭 인터페이스·타입
// API 응답 래퍼
interface ApiResponse<T> {
ok: boolean;
data: T;
error?: string;
}
// 사용
type UserResp = ApiResponse<{ id: number; name: string }>;
type PostsResp = ApiResponse<Array<{ id: number; title: string }>>;
// 클래스에도
class Box<T> {
constructor(public value: T) {}
get(): T { return this.value; }
}
const stringBox = new Box("hi"); // Box<string> 추론
const numBox = new Box<number>(0);
흔한 함정 — 화살표 함수의 <T>. JSX/TSX 파일에서 const f = <T>(x: T) => x 는 JSX 태그로 오해됩니다. 쉼표로 해결 — <T,>(x: T) => x, 또는 extends 사용 — <T extends unknown>(x: T) => x. 일반 .ts 파일에서는 문제 없습니다.
제네릭 작성의 3가지 원칙
- 타입 매개변수는 최소한으로. 안 쓰면 의미 없는 잡음.
- 입력과 출력에 모두 등장해야 가치가 생긴다. 한쪽에만 쓰는 T 는 보통 any 로 대체 가능 — 다시 생각.
- 제약을 적절히. 너무 넓으면 본문에서 쓸 수 있는 게 없고, 너무 좁으면 호출자가 답답하다.
7편 — enum 과 const enum
상태 코드·테마 같은 분류값에 enum 을 쓸지, union literal 을 쓸지의 결정.
📚 쉽게 배우는 타입스크립트 교재
이전: 5편 유니온·인터섹션 · 현재: 6편 (기초) · 다음 → 7편 enum · 진행: 6/20
이전: 5편 유니온·인터섹션 · 현재: 6편 (기초) · 다음 → 7편 enum · 진행: 6/20