
제네릭(Generic), 도대체 `T`가 뭔가요?
TypeScript를 배우다 보면 만나는 `Function<T>`. 외계어 같던 제네릭을 '투명 스티커'와 '자판기' 비유로 완벽하게 이해하고, `any`와의 차이점을 정리해봤습니다.

TypeScript를 배우다 보면 만나는 `Function<T>`. 외계어 같던 제네릭을 '투명 스티커'와 '자판기' 비유로 완벽하게 이해하고, `any`와의 차이점을 정리해봤습니다.
로버트 C. 마틴(Uncle Bob)이 제안한 클린 아키텍처의 핵심은 무엇일까요? 양파 껍질 같은 계층 구조와 의존성 규칙(Dependency Rule)을 통해 프레임워크와 UI로부터 독립적인 소프트웨어를 만드는 방법을 정리합니다.

서로 다른 인터페이스를 연결해주는 변환기. 레거시 시스템과 신규 시스템을 이어주는 가장 강력한 디자인 패턴.

객체지향의 거장 로버트 마틴(Uncle Bob)이 정립한 5가지 설계 원칙. SRP, OCP, LSP, ISP, DIP가 무엇인지, 왜 지켜야 하는지, 실제 타입스크립트 예제로 정리해본다.

나만 알아보는 코드는 쓰레기입니다. 변수명 짓기부터 함수 쪼개기, 그리고 주석을 달지 말아야 하는 이유까지. 6개월 뒤의 나를 살리는 리팩토링의 기술.

<T>가 나오는 순간 뇌가 멈췄습니다"TypeScript 문법을 공부하다가 함수까지는 괜찮았습니다.
그런데 갑자기 꺾쇠 괄호 <T>가 등장하더니, 코드가 외계어처럼 보이기 시작했습니다.
function echo<T>(arg: T): T {
return arg;
}
"그냥 any 쓰면 되는 거 아니야? 왜 굳이 저런 복잡한 기호를 써야 해?"
저는 제네릭을 '고수들만 쓰는 어려운 문법'이라고 단정 짓고 피해 다녔습니다.
하지만 라이브러리를 쓰거나 실제 코드를 보면 온통 제네릭 투성이였습니다.
결국 피할 수 없는 산이었습니다.
저는 프로그래밍에서 "타입을 정한다"는 건 "확실하게 하는 것"이라고 생각했습니다.
string, number처럼 딱 정해져야 마음이 편했죠.
그런데 제네릭은 "뭐가 들어올지 모른다"면서도 any는 아니라고 합니다.
"모르는데 어떻게 타입 체크를 해? 그게 any랑 뭐가 달라?"
이 모순적인 개념이 받아들여지지 않았습니다.
이걸 "자판기와 투명 스티커"에 비유하니 이해가 됐습니다.
any: 검은 봉지입니다. 안에 뭐가 들었는지 아예 안 보입니다. 사과를 넣었는데 꺼내보니 벽돌일 수도 있습니다. (불안함)<T>): 투명한 비닐봉지입니다. 아직 뭐가 들어갈지는 모르지만, "넣은 그대로 보인다"는 건 확실합니다. 사과를 넣으면 빨간 사과가 보이고, 배를 넣으면 노란 배가 보입니다.// any: 넣을 땐 맘대로지만, 꺼낼 땐 뭔지 모름 (위험)
function anyBox(item: any): any {
return item;
}
const box1 = anyBox(10); // box1은 any 타입.
// Generic: 넣는 순간 타입이 결정됨 (안전)
function genericBox<T>(item: T): T {
return item;
}
const box2 = genericBox(10); // box2는 number 타입! (TS가 알아냄)
제네릭은 "타입을 미리 정하는 게 아니라, 사용하는 시점에 타입을 변수처럼 넘겨주는 것"이었습니다. 즉, "타입을 위한 변수(Type Variable)"였던 겁니다.
가장 흔한 예제는 API 호출 함수입니다. 서버 응답이 어떤 모양일지 함수를 만들 땐 모르지만, 쓸 땐 알 수 있으니까요.
async function fetchJson<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json();
}
// 사용할 때 타입(T)을 '주입'해줌
interface User { name: string }
const user = await fetchJson<User>('/api/user');
// 이제 user.name 자동 완성이 됩니다!
extends)T라고 해서 아무거나 다 받는 건 싫을 때가 있습니다.
"최소한 length 속성은 있어야 해!"라고 조건을 걸 수 있습니다.
// T는 반드시 length가 있는 타입이어야 함 (string, array 등)
function logLength<T extends { length: number }>(arg: T) {
console.log(arg.length);
}
logLength("hello"); // OK (문자열은 length 있음)
logLength([1, 2]); // OK (배열도 length 있음)
logLength(10); // Error! (숫자는 length 없음)
이 extends 키워드가 제네릭의 꽃입니다. 무한한 자유가 아니라 안전한 자유를 보장해주니까요.
Select 박스)리액트에서 공용 컴포넌트 만들 때 제네릭이 필수입니다.
interface SelectProps<T> {
options: T[];
value: T;
onChange: (value: T) => void;
}
// <T,> : 화살표 함수에서 제네릭 쓸 때 JSX 태그랑 헷갈리지 말라고 콤마 붙임
const Select = <T,>({ options, value, onChange }: SelectProps<T>) => {
return (
// ... 구현
);
};
// 사용: 문자열 선택기
<Select<string> options={["A", "B"]} value="A" ... />
// 사용: 숫자 선택기
<Select<number> options={[1, 2]} value=1 ... />
제네릭이 없었다면 StringSelect, NumberSelect를 따로 만들거나 any로 도배해야 했을 겁니다.
우리가 자주 쓰는 Partial<T>, Pick<T, K>, Record<K, T>...
이런 내장 유틸리티 타입들도 까보면 다 제네릭으로 만들어져 있습니다.
// 실제 Partial의 정의 (비슷함)
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
"모든 속성(keyof T)을 순회(in)하면서 물음표(?)를 붙여라."
제네릭을 이해하면 이런 유틸리티 타입을 직접 만들어 쓸 수도 있습니다. (이걸 '타입 체조'라고 부르죠.)
infer 키워드 (제네릭 안의 타입을 꺼내오기) 제대로 파보기제네릭의 끝판왕, infer입니다.
이것은 "조건부 타입(Conditional Type)" 안에서 쓰이는데, "타입을 유추해서 변수처럼 뽑아내라"는 뜻입니다.
가장 유명한 ReturnType 유틸리티가 이렇게 만들어졌습니다.
// T가 함수라면, 그 리턴 타입을 R이라고 부르고, R을 반환해라. 아니면 Any.
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
function getUser() { return { name: "Kim", age: 30 }; }
// UserType은 자동으로 { name: string, age: number }가 됨!
type UserType = MyReturnType<typeof getUser>;
이걸 이해하면 라이브러리 코드를 읽을 때 "신세계"가 열립니다. "입력된 함수가 뱉는 타입을 뽑아서, 다음 함수의 인자 타입으로 써라" 같은 마법이 가능해집니다.
회사 내부용 Axios 래퍼를 만들 때였습니다.
API 응답이 항상 timestamp, code, data 구조로 오는데, data 안에 또 다른 중첩된 객체가 들어올 수 있었습니다.
제네릭을 한 겹만 썼더니 깊은 곳의 타입이 깨졌습니다.
타입도 자기 자신을 호출할 수 있습니다.
type JsonValue = string | number | boolean | null | JsonArray | JsonObject;
interface JsonObject { [key: string]: JsonValue; } // 나 자신을 참조
interface JsonArray extends Array<JsonValue> {} // 나 자신을 참조
// 사용
const data: JsonObject = {
user: {
posts: [ { id: 1, title: "Hello" } ] // 무한히 깊어져도 타입 체크 가능
}
};
이렇게 정의해두면 data.user.posts[0].title까지 자동 완성이 지원됩니다.
제네릭은 구조(Structure)를 정의하는 언어입니다.
T 말고 다른 거)T, U, V... 수학 시간도 아니고 너무 헷갈립니다.
코드의 가독성을 위해 의미 있는 이름을 쓰세요.
T -> TData (데이터)R -> TResult (결과)P -> TProps (속성)E -> TError (에러)function fetchAPI<TData, TError>(url: string) { ... }
훨씬 읽기 좋습니다. 팀원을 배려하세요.
keyof) 제대로 이해하기"객체에서 특정 키의 값만 뽑아오는 함수"를 만들 때 keyof가 필수입니다.
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
const user = { name: "Alice", age: 25 };
getProperty(user, "name"); // OK
getProperty(user, "email"); // Error: "email"은 "name" | "age"에 없음!
이걸 통과하면 여러분은 제네릭 중급자입니다.
K extends keyof T는 "K는 T가 가진 열쇠(Key)들 중 하나여야 한다"는 뜻입니다. 오타 방지에 최고입니다.
Pick과 Omit 직접 구현하기자주 나오는 질문입니다. "Pick을 직접 구현해보세요."
// 1. Mapped Type: K에 있는 것들만 루프 돌림
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
// 2. Exclude 활용: 전체 키에서 K를 뺀 나머지를 구함
type MyOmit<T, K extends keyof T> = MyPick<T, Exclude<keyof T, K>>;
이걸 외울 필요는 없지만, 원리를 알면 복잡한 비즈니스 로직 타입을 설계할 때 큰 도움이 됩니다. "API 응답에서 민감한 필드만 빼고 프론트에 넘겨주는 타입" 같은 걸 짤 때 말이죠.