타입스크립트 교재 · 6편 / 20편

제네릭 입문 — <T> 한 글자의 힘

제네릭은 "타입을 변수처럼 받는 함수". 라이브러리 코드의 80% 가 여기서 나옵니다.

기초읽는 시간 7분2026-05-17
<T> 한 글자가 다양한 타입으로 변환되는 모습의 도식

이 한 줄을 본 적 있나요?

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가지 원칙

  1. 타입 매개변수는 최소한으로. 안 쓰면 의미 없는 잡음.
  2. 입력과 출력에 모두 등장해야 가치가 생긴다. 한쪽에만 쓰는 T 는 보통 any 로 대체 가능 — 다시 생각.
  3. 제약을 적절히. 너무 넓으면 본문에서 쓸 수 있는 게 없고, 너무 좁으면 호출자가 답답하다.

7편 — enum 과 const enum

상태 코드·테마 같은 분류값에 enum 을 쓸지, union literal 을 쓸지의 결정.

📚 쉽게 배우는 타입스크립트 교재
이전: 5편 유니온·인터섹션 · 현재: 6편 (기초) · 다음 → 7편 enum · 진행: 6/20

© 2026 주나이테크(주) @JUNAITECH