
함수나 컴포넌트를 만들다 보면, 타입이 고정되지 않고 입력값에 따라 더 유연하게 타입을 다뤄야 하는 상황이 있어요. 이럴 때 제네릭을 사용하면 타입의 안정성까지 챙기면서 깔끔하게 해결할 수 있습니다.
TypeScript 제네릭의 기본 개념과 실무에서 어떻게 활용할 수 있는지 정리해보았습니다.
TypeScript에서 제네릭(Generic)은 타입을 변수처럼 사용할 수 있는 기능으로 컴포넌트(함수, 클래스, 인터페이스 등)를 선언할 때 타입을 고정하지 않고 실제 사용할 때 타입을 외부에서 주입받아 다양한 타입을 처리할 수 있도록 만든다.
function identity<T>(value: T): T {
return value;
}
T는 타입 매개변수 (Type Parameter)로 이 함수는 호출하는 시점에 T가 어떤 타입인지 결정된다.

타입을 지정하기가 어려울 때 보통 any를 사용하게되는데 any는 타입 검사를 우회해버리기 때문에 타입스크립트를 사용하는 이점이 사라진다. 제네릭은 타입을 동적으로 받을 수 있으면서도 타입 검사를 유지할 수 있다.

위 코드와 같이 any를 사용하게되면 타입스크립트는 타입 검사를 하지 않아 실제로 number를 넘겼더라도 타입스크립트는 이를 체크하지 않아서 toUpperCase() 같은 잘못된 메서드를 호출해도 타입 에러가 발생하지 않는다.
하지만 런타임에서는 TypeError: 100.toUpperCase is not a function 에러가 발생할 수 있다.
제네릭을 사용하면 타입스크립트가 타입 추론을 정확하게 수행하고, 잘못된 메서드 호출을 컴파일 단계에서 바로 잡을 수 있다.

T는 호출 시점에 number로 추론이 되어 result[0]의 타입이 명확하게 number로 추론되기 때문에 toUpperCase() 호출 시 타입스크립트가 바로 에러를 잡아준다.// 결제 정보와 배송 정보를 병합하는 함수
function mergePaymentAndShipping<T, U>(payment: T, shipping: U): T & U {
return { ...payment, ...shipping };
}
const paymentInfo = { paymentId: 'PAY123', amount: 120000, method: 'CreditCard' };
const shippingInfo = { trackingNumber: 'TRK456', courier: '택배사' };
const orderDetail = mergePaymentAndShipping(paymentInfo, shippingInfo);
여기서 T는 첫 번째 인자(payment)의 타입, U는 두 번째 인자(shipping)의 타입을 의미한다. 제네릭은 필요하면 여러 개의 타입 매개변수를 동시에 받을 수 있다.
이 함수는 결제 데이터와 배송 데이터처럼 서로 다른 구조의 타입을 병합할 때 유용하다.
결제 정보와 배송 정보를 모두 포함한 하나의 새로운 객체를 반환하며,
타입스크립트는 반환 객체가 결제 타입 + 배송 타입의 속성을 모두 가진다는 것을 정확히 추론한다.
T & U의 의미
반환 타입 T & U는 교차 타입(Intersection Type) 이다.교차 타입이란?
두 타입을 모두 만족하는 새로운 타입을 생성하는 것이다.
쉽게 말하면 결제 타입 + 배송 타입이 합쳐진 확장된 타입을 의미한다.
결제 정보와 배송 정보를 모두 포함한 하나의 새로운 객체를 반환하며 타입스크립트는 반환 객체가 결제 타입 + 배송 타입의 속성을 모두 가진다는 것을 정확히 추론한다.

🤔 제네릭 API 응답 패턴을 주로 사용하는 이유
1. 대부분 API 응답 포맷이 동일하다.
{ "status": 200, "data": { ... }, "message": "Success" }실무에서 대부분의 API는 위처럼 공통 응답 포맷을 사용하고, 실제로 바뀌는 부분은 data 타입뿐이다. 이럴 때 제네릭을 사용하면 공통 응답 구조는 한 번만 선언하고, data 타입만 유연하게 바꿔서 재사용할 수 있다.
2. 중복 선언을 줄일 수 있다
API마다 응답 타입을 일일이 선언하면 타입 파일이 불필요하게 늘어나고, 추후 응답 구조가 바뀌었을 때 중복 수정이 많이 발생해 유지보수 부담이 커진다.제네릭을 사용하면 공통 스키마를 단일화하고, 각 API별로 바뀌는 data만 타입 주입으로 관리하면 된다.
const orderResponse: ApiResponse<OrderDetail> = { status: 200, data: { ... } }; const shippingResponse: ApiResponse<ShippingData> = { status: 200, data: { ... } };3. API 통신 유틸과 잘 맞는다
실무에서는 보통 API를 여러 군데에서 반복해서 호출하지 않기 위해
공통 fetch 함수를 따로 만들어 재사용하는 경우가 많다.// 공통 fetch 함수 예시 async function fetchData<T>(url: string): Promise<ApiResponse<T>> { const res = await fetch(url); return res.json(); } // 결제 응답 데이터 타입 type PaymentData = { paymentId: string; paymentStatus: 'SUCCESS' | 'FAILURE'; }; // 결제 API를 호출 const res = await fetchData<PaymentData>('/api/payment');
fetchData<T>는 제네릭 함수로fetchData<PaymentData>는 "이번 호출에서는 결제 데이터 타입이 올 거야!" 라고 타입스크립트에게 명확하게 알려주는 것이다.이렇게 제네릭 타입을 주입하면, 타입스크립트가
res.data를 정확히PaymentData타입으로 추론해서 자동완성 지원, 타입 안전성, 잘못된 속성 접근 방지까지 모두 보장할 수 있다.만약 API마다
status,message등 응답 구조 자체가 완전히 다르거나, 레거시 API처럼 공통 스키마가 아예 없는 경우에는
이런 제네릭 공통 패턴 대신, 각 API에 맞는 개별 타입 선언이 더 안전할 수도 있다. 😢
공통 응답 스키마는 제네릭으로 추상화해도, 각 API별로 data 타입은 명확히 선언해줘야 한다. 만약 data 타입을 any로 넘기거나 대충 작성하면 타입스크립트의 장점을 제대로 못 쓴다.
// ❌
const res = await fetchData<any>('/api/payment'); // 타입 추론 X
// ✅
const res = await fetchData<PaymentData>('/api/payment'); // 타입 추론 O
타입스크립트가 때로는 API 응답을 추론할 수도 있지만, API 문서가 불완전할 때 또는 백엔드 API 응답 스펙이 자주 바뀌는 경우 타입을 명시적으로 주입하는 것이 훨씬 더 안전하다.
// 안전하지 않은 예시 (타입 명시 안 함)
const res = await fetchData('/api/payment');
// 안전한 예시 (타입 직접 주입)
const res = await fetchData<PaymentData>('/api/payment');
잘못된 데이터 접근을 컴파일 타임에서 빠르게 잡아낼 수 있다.
또는 fetchData 함수는 항상 Promise<ApiResponse<T>>처럼
타입을 반환까지 강제하는 패턴으로 설계하는 것도 안정성이 높아진다.
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
const res = await fetch(url);
return res.json();
}
T, U 같은 추상적인 네이밍을 쓰기보다는, ResponseData, PaymentType처럼 실제 데이터의 의미를 담은 이름이 가독성에 좋다.
async function fetchData<ResponseData>(url: string): Promise<ApiResponse<ResponseData>> { ... }
상황에 맞게 제네릭을 적절히 사용하면 공통 패턴을 잘 활용할 수 있어 API 타입 선언이 훨씬 깔끔해지고 협업에서도 안정성 있는 코드로 이어질 수 있다. 💪🏻
이 글은 공식 문서를 기반으로 내용을 정리한 포스팅입니다.
혹시 내용 중 틀린 부분이나 보완할 부분이 있다면 댓글로 남겨주시면 감사하겠습니다. 🙏🏻