올리브영 테크블로그 포스팅 올리브영 타입스크립트로 알아보는 제네릭과 매개변수 다형성
Tech

올리브영 타입스크립트로 알아보는 제네릭과 매개변수 다형성

타입을 추상화하는 또 다른 방법

2025.12.31

지난 아티클 타입스크립트로 알아보는 타입과 타입 시스템에서 우리는 타입 시스템의 기초를 탄탄히 다졌습니다. 타입 검사기가 어떻게 프로그램의 모순을 찾아내는지, 그리고 서브타입에 의한 다형성이 딱딱한 타입에 유연성을 어떻게 부여하는지 살펴보았죠.

서브타입에 의한 다형성 덕분에 TodayProduct 타입의 값을 Product 타입이 필요한 곳에 전달할 수 있게 되었습니다. "(올리브영의 당일 배송 서비스인) 오늘드림 상품도 상품이다"라는 직관적인 관계가 타입 시스템에서도 인정받게 된 것이죠.

그런데 한 가지 의문이 듭니다.

서브타입 관계가 없는, 완전히 독립적인 타입들에 동일한 로직을 적용하려면 어떻게 해야 할까요?

이번 글에서도 올리브영의 실무 코드를 바탕으로, 타입의 한계를 뛰어넘어 유연성을 극대화하는 방법을 함께 파헤쳐 보겠습니다.

같은 로직, 다른 타입

올리브영 서비스에는 수많은 데이터 모델이 존재합니다. 상품, 리뷰, 주문 내역은 각각 그 목적에 맞게 서로 다른 속성들을 가지고 있죠.

interface Product {
  id: string;
  name: string;
  price: number; // 상품은 가격이 중요합니다.
}

interface Review {
  id: string;
  content: string;
  rating: number; // 리뷰는 별점이 핵심이죠.
}

interface Order {
  id: string;
  orderDate: Date; // 주문은 언제 했는지가 중요합니다.
}

이렇게 데이터의 모양이 제각각인 상황에서, 목록의 첫 번째 요소를 가져오는 함수가 필요하다고 가정해봅시다.

function getFirstProduct(items: Product[]): Product | undefined {
  return items[0];
}

잘 작동합니다. 그런데 이번엔 리뷰 목록의 첫 번째 요소도 가져오고 싶습니다.

function getFirstReview(items: Review[]): Review | undefined {
  return items[0];
}

주문 내역의 첫 번째 요소도요.

function getFirstOrder(items: Order[]): Order | undefined {
  return items[0];
}

세 함수의 본문을 보세요. return items[0];로 모두 동일합니다. 로직은 하나인데, 타입이 다르다는 이유만으로 함수를 세 개나 만들어야 합니다.

ProductReviewOrder 사이에는 서브타입 관계가 없습니다. "상품은 리뷰이다"라거나 "주문은 상품이다"라는 명제는 성립하지 않으니까요. 서브타입에 의한 다형성으로는 이 문제를 해결할 수 없습니다.

우리에겐 새로운 도구가 필요합니다. 타입 자체를 추상화하는 도구, 바로 제네릭입니다.


제네릭이란?

제네릭(Generics)은 타입 변수(Type Variable)를 사용하여 타입을 추상화하는 방법입니다.

함수가 값을 매개변수로 받아 다양한 값에 대해 같은 로직을 수행하듯이, 제네릭은 타입을 매개변수로 받아 다양한 타입에 대해 같은 로직을 수행합니다.

앞서 만든 세 개의 함수를 제네릭으로 합쳐봅시다.

function getFirst<T>(items: T[]): T | undefined {
  return items[0];
}

<T>가 바로 타입 변수입니다. 이 함수는 "어떤 타입 T의 배열을 받아서, 그 T 타입의 값을 반환한다"고 선언합니다.

이제 하나의 함수로 모든 경우를 처리할 수 있습니다.

const firstProduct = getFirst<Product>(products);  // Product | undefined
const firstReview = getFirst<Review>(reviews);      // Review | undefined
const firstOrder = getFirst<Order>(orders);         // Order | undefined

함수를 호출할 때 <Product>, <Review>, <Order>처럼 타입 인자를 전달합니다. 마치 함수에 값을 전달하듯이요.

여기서 중요한 점이 있습니다. 제네릭 없이 any를 사용해도 비슷해 보이지만, 결정적인 차이가 있습니다.

// any 사용 - 타입 정보 손실
function getFirstAny(items: any[]): any {
  return items[0];
}
const first = getFirstAny(products); // any - 타입 정보가 사라짐

// 제네릭 사용 - 타입 정보 보존
function getFirst<T>(items: T[]): T | undefined {
  return items[0];
}
const first = getFirst(products); // Product | undefined - 타입 정보 유지

any를 사용하면 함수를 통과하는 순간 타입 정보가 증발해 버립니다. 반환값이 any이니 이후 어떤 속성에 접근해도 타입 검사기는 아무런 도움을 주지 못합니다.

반면 제네릭을 사용하면 타입 정보가 함수를 관통하여 보존됩니다. Product[]를 넣으면 Product가 나오고, Review[]를 넣으면 Review가 나옵니다. 타입 검사기의 보호를 계속 받을 수 있습니다.


일반 함수에서는 타입 정보가 손실되고, 제네릭 함수에서는 타입 정보가 보존되는 흐름을 보여주는 다이어그램

[그림 1: 일반 함수 vs 제네릭 함수 - 타입 정보의 흐름]

타입 인자 추론

사실 대부분의 경우, 타입 인자를 명시적으로 적을 필요가 없습니다.

const firstProduct = getFirst(products); // T가 Product로 자동 추론됨

타입스크립트 컴파일러는 products의 타입이 Product[]임을 알고 있습니다. 이 정보를 바탕으로 TProduct임을 스스로 추론합니다. 이를 타입 인자 추론(Type Argument Inference)이라고 합니다.

타입 인자 추론 덕분에 제네릭 코드도 일반 코드처럼 간결하게 작성할 수 있습니다. 컴파일러가 추론에 실패하는 복잡한 경우에만 명시적으로 타입 인자를 적어주면 됩니다. (더 자세한 내용은 타입스크립트 공식 핸드북을 참고하세요.)


제네릭의 필요성 - 올리브영 API 응답

제네릭의 진가는 실무에서 더욱 빛납니다. 올리브영의 API 응답 구조를 생각해봅시다.

상품 목록 API, 사용자 정보 API, 주문 내역 API 모두 비슷한 응답 구조를 가집니다. 실제 데이터(data)와 함께 상태 코드(status), 메시지(message)를 반환하죠.

// 제네릭 없이 - 타입마다 별도 정의
interface ProductResponse {
  data: Product;
  status: number;
  message: string;
}

interface UserResponse {
  data: User;
  status: number;
  message: string;
}

interface OrderResponse {
  data: Order;
  status: number;
  message: string;
}

interface ReviewListResponse {
  data: Review[];
  status: number;
  message: string;
}

// ... API가 늘어날 때마다 새로운 타입 정의

statusmessage 부분이 계속 반복됩니다. API가 수십 개라면 수십 번 같은 코드를 작성해야 합니다.

제네릭으로 이 반복을 제거해봅시다.

// 제네릭으로 - 하나의 타입으로 모든 응답 처리
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

// 사용
type ProductResponse = ApiResponse<Product>;
type UserResponse = ApiResponse<User>;
type OrderResponse = ApiResponse<Order>;
type ReviewListResponse = ApiResponse<Review[]>;

ApiResponse<T>라는 하나의 제네릭 인터페이스가 모든 API 응답 타입을 생성합니다. T 자리에 실제 데이터 타입을 넣으면 완전한 응답 타입이 만들어집니다.

이제 API 응답 구조가 변경되어도 ApiResponse<T> 한 곳만 수정하면 됩니다. 모든 응답 타입에 일괄 적용되니까요.


제네릭 없이 여러 타입을 반복 정의하던 코드가 제네릭으로 하나로 통합되는 과정

[그림 2: 제네릭으로 코드 중복 제거]

제네릭 함수와 타입 추론

제네릭 함수를 좀 더 살펴봅시다. 다양한 목록을 필터링하는 상황을 생각해보세요.

// 설명을 위해 단순화한 예시입니다.
function filterItems<T>(items: T[], predicate: (item: T) => boolean): T[] {
  return items.filter(predicate);
}

이 함수는 "어떤 타입 T의 배열과, T를 받아 불리언을 반환하는 함수를 받아서, T의 배열을 반환한다"고 선언합니다.

올리브영에서 사용해볼까요?

// Product에는 price, Review에는 rating, Order에는 date 속성이 있다고 가정

// 5만원 이상 상품 필터링
const expensiveProducts = filterItems(products, (p) => p.price > 50000);

// 4점 이상 리뷰 필터링
const goodReviews = filterItems(reviews, (r) => r.rating >= 4);

// 이번 달 주문 필터링
const thisMonthOrders = filterItems(orders, (o) => o.date.getMonth() === new Date().getMonth());

세 번의 호출 모두 타입 인자를 명시하지 않았습니다. 그런데 콜백 함수 안에서 p.price, r.rating, o.date에 접근할 때 자동완성이 작동합니다. 타입 검사도 정상적으로 이루어집니다.

어떻게 가능할까요? 타입스크립트가 products의 타입에서 TProduct로 추론했기 때문입니다. 그 추론이 콜백 함수의 매개변수 p에까지 전파되어, pProduct 타입임을 알게 됩니다.

이것이 제네릭과 타입 추론의 시너지입니다. 타입을 명시하지 않아도 타입 안전성을 누릴 수 있습니다.


제네릭 제약

제네릭의 강력함에는 대가가 따릅니다. T가 무엇이든 될 수 있기 때문에, T에 특정 속성이 있다고 가정할 수 없습니다.

function getLength<T>(arg: T): number {
  return arg.length; // Error: Property 'length' does not exist on type 'T'
}

Tnumber일 수도 있고, boolean일 수도 있습니다. 이들에게는 length 속성이 없죠. 타입 검사기는 가능한 모든 경우를 고려하므로, 이 코드를 허용하지 않습니다.

하지만 우리는 '길이' 속성이 있는 타입에 대해서만 이 함수를 사용하고 싶습니다. 이때 제네릭 제약(Generic Constraints)을 사용합니다.

interface HasLength {
  length: number;
}

function getLength<T extends HasLength>(arg: T): number {
  return arg.length; // OK!
}

T extends HasLength는 "THasLength의 서브타입이어야 한다"는 제약입니다. 이제 T는 최소한 length 속성을 가진 타입만 될 수 있습니다.

getLength("hello");      // OK - string은 length가 있음
getLength([1, 2, 3]);    // OK - 배열도 length가 있음
getLength({ length: 10 }); // OK - length 속성이 있는 객체

getLength(123);          // Error - number는 length가 없음
getLength(true);         // Error - boolean도 length가 없음

여기서 서브타입에 의한 다형성과 매개변수에 의한 다형성이 만납니다. extends 키워드가 그 접점입니다.

올리브영 예제로 적용해봅시다. 가격이 있는 모든 것에 할인을 적용하는 함수를 만들어보겠습니다.

// 설명을 위해 단순화한 예시입니다.
interface HasPrice {
  price: number;
}

function applyDiscount<T extends HasPrice>(item: T, rate: number): T {
  return {
    ...item,
    price: item.price * (1 - rate / 100)
  };
}

이 함수는 price 속성을 가진 어떤 타입이든 받을 수 있습니다.

const discountedProduct = applyDiscount(product, 10);     // Product
const discountedTodayProduct = applyDiscount(todayProduct, 15); // TodayProduct
const discountedGiftSet = applyDiscount(giftSet, 20);     // GiftSet

Product, TodayProduct, GiftSet 모두 price 속성을 가지고 있다면 이 함수를 사용할 수 있습니다. 그리고 반환 타입은 입력 타입과 동일하게 유지됩니다. TodayProduct를 넣으면 TodayProduct가 나옵니다.


extends 키워드로 제네릭 타입 T의 범위를 특정 인터페이스의 서브타입으로 제한하는 다이어그램

[그림 3: 제네릭 제약으로 타입 범위 한정]

제네릭과 서브타입의 만남 - 변성 입문

지금까지 제네릭의 기초를 살펴보았습니다. 이제 조금 더 깊이 들어가봅시다.

지난 글에서 우리는 서브타입 관계를 배웠습니다. TodayProductProduct의 서브타입입니다. "오늘드림 상품은 상품이다"가 성립하니까요.

그렇다면 질문을 던져봅시다.

TodayProductProduct의 서브타입일 때, Array<TodayProduct>Array<Product>의 서브타입일까요?

직관적으로는 "그렇다"고 답하고 싶습니다. 오늘드림 상품의 배열은 상품의 배열이니까요.

하지만 이 질문의 답은 생각보다 복잡합니다. 그리고 이 복잡함을 설명하는 개념이 바로 변성(Variance)입니다.

변성의 세 가지 종류

타입 A가 타입 B의 서브타입일 때, 제네릭 타입 F<A>F<B>의 관계는 크게 세 가지로 나뉩니다. (타입스크립트에는 이변성이라는 네 번째 종류도 있는데, 뒤에서 다룹니다.)

1. 공변성 (Covariance)

A가 B의 서브타입이면, F<A>도 F<B>의 서브타입이다.

타입 인자의 서브타입 관계가 그대로 유지됩니다.

TodayProduct <: Product
     ↓ 관계 유지
Array<TodayProduct> <: Array<Product>

직관과 일치하죠. 오늘드림 상품이 상품의 서브타입이니, 오늘드림 상품 배열도 상품 배열의 서브타입입니다.


공변성: TodayProduct가 Product의 서브타입일 때 Array<TodayProduct>도 Array<Product>의 서브타입이 되는 관계

[그림 4: 공변성 - 서브타입 관계가 유지된다]

2. 반공변성 (Contravariance)

A가 B의 서브타입이면, F<B>가 F<A>의 서브타입이다.

타입 인자의 서브타입 관계가 뒤집힙니다.

TodayProduct <: Product
     ↓ 관계 역전
Consumer<Product> <: Consumer<TodayProduct>

// Consumer<T>는 T를 "소비"하는 타입, 즉 T를 매개변수로 받는 함수 타입입니다
// type Consumer<T> = (item: T) => void

이건 좀 낯섭니다. 하지만 지난 글에서 이미 이 개념을 만났습니다. 기억하시나요?

함수 타입은 매개변수 타입의 서브타입 관계를 뒤집는다.

바로 이것이 반공변성입니다. 상품을 처리하는 함수는 오늘드림 상품도 처리할 수 있기에, 상품 함수는 오늘드림 상품 함수의 서브타입이 됩니다.


반공변성: TodayProduct가 Product의 서브타입일 때 Consumer<Product>가 Consumer<TodayProduct>의 서브타입이 되어 관계가 역전됨

[그림 5: 반공변성 - 서브타입 관계가 뒤집힌다]

3. 불변성 (Invariance)

A와 B가 다른 타입이면, F<A>와 F<B> 사이에 서브타입 관계가 없다.

정확히 같은 타입만 허용됩니다. 서브타입도 안 되고, 슈퍼타입도 안 됩니다. 오늘드림 상품과 상품은 엄연히 다른 타입이기에, 둘 사이에는 어떤 서브타입 관계도 인정하지 않습니다.


불변성: A와 B가 다른 타입이면 F<A>와 F<B> 사이에 어떤 서브타입 관계도 성립하지 않음

[그림 6: 불변성 - 서브타입 관계가 없다]

왜 변성이 중요한가?

변성이 왜 필요한지, 배열을 예로 들어 살펴봅시다.

배열이 공변적이라고 가정해봅시다. 즉, Array<TodayProduct>Array<Product>의 서브타입입니다.

const todayProducts: TodayProduct[] = [
  { title: "탈모 샴푸", price: 25000, isTodayDelivery: true },
  { title: "마스크팩", price: 15000, isTodayDelivery: true }
];

// 공변성에 의해 이 할당이 허용된다고 가정
const products: Product[] = todayProducts;

여기까지는 문제없어 보입니다. 읽기만 한다면요.

// 읽기 - 안전함
console.log(products[0].title); // OK

하지만 쓰기를 하면 어떨까요?

// 쓰기 - 위험!
products.push({ title: "일반 상품", price: 10000 });
// isTodayDelivery 속성이 없는 일반 Product

// todayProducts 배열에 TodayProduct가 아닌 값이 들어감!
todayProducts.forEach(p => {
  console.log(p.isTodayDelivery); // undefined - 런타임 에러 가능!
});

productstodayProducts는 같은 배열을 참조합니다. products를 통해 일반 Product를 추가하면, todayProducts에도 그 값이 들어갑니다. 하지만 todayProducts는 모든 요소가 TodayProduct라고 믿고 있죠. 타입 시스템이 깨지는 순간입니다.

이것이 변성이 중요한 이유입니다. 읽기만 하는 컨테이너는 공변적이어도 안전하지만, 쓰기도 하는 컨테이너는 공변적이면 위험합니다.


타입스크립트의 변성 처리

타입스크립트는 이 복잡한 변성을 어떻게 처리할까요? 타입 안전성과 실용성 사이에서 고민한 타입스크립트만의 현실적인 절충안을 여기서 엿볼 수 있습니다.

배열은 공변적으로 처리된다

앞서 살펴본 것처럼, 쓰기가 가능한 배열이 공변적인 것은 타입 안전하지 않습니다(unsound). todayProducts 배열을 products로 참조한 뒤 일반 Product를 추가하면, 타입 시스템이 보장하던 안전성이 깨지죠.

그럼에도 타입스크립트는 배열을 공변적으로 처리합니다.

function printProductTitles(products: Product[]) {
  products.forEach(p => console.log(p.title));
}

const todayProducts: TodayProduct[] = [/* ... */];
printProductTitles(todayProducts); // OK - 허용됨

왜일까요? 실용성 때문입니다.

위 코드처럼 배열을 읽기 전용으로 사용하는 패턴은 자바스크립트에서 매우 흔합니다. 이 패턴을 에러로 처리하면 수많은 정상적인 코드가 컴파일되지 않습니다.

타입스크립트는 완벽한 타입 안전성보다 실용성을 선택했습니다. 대신 개발자가 위험한 쓰기를 하지 않을 것이라고 신뢰합니다.

함수 타입의 변성

배열에서는 실용성을 위해 공변성을 허용했습니다. 그렇다면 함수 타입은 어떨까요? 함수 타입은 좀 더 엄격합니다. strictFunctionTypes 옵션이 켜져 있다면 타입스크립트는 다음과 같이 동작합니다.

  • 반환 타입: 공변적 (서브타입 관계 유지)
  • 매개변수 타입: 반공변적 (서브타입 관계 역전)

매개변수 반공변성이 낯설게 느껴진다면, 지난 글의 예제를 떠올려보세요. "함수 타입은 매개변수 타입의 서브타입 관계를 뒤집는다"고 했었죠. 왜 그래야 하는지, 만약 공변적이라면 어떤 일이 벌어지는지 살펴봅시다.

type TodayProductHandler = (p: TodayProduct) => void;

// TodayProduct의 isTodayDelivery 속성에 접근하는 함수
const processTodayProduct: TodayProductHandler = (p) => {
  console.log(`오늘 배송 여부: ${p.isTodayDelivery}`);
};

// 만약 매개변수가 공변적이라면, 이 할당이 허용될 것입니다
// (TodayProduct <: Product 이므로 TodayProductHandler <: ProductHandler?)
const processProduct: ProductHandler = processTodayProduct; // 실제로는 에러!

// 일반 Product를 전달하면?
processProduct({ title: "일반 상품", price: 10000 });
// isTodayDelivery 속성이 없음 - 런타임 에러!

processTodayProductisTodayDelivery 속성이 있다고 가정하고 동작합니다. 그런데 일반 Product에는 이 속성이 없죠. 매개변수가 공변적이면 타입 시스템을 통과하지만 런타임에 실패합니다.

반대 방향은 안전합니다.

type ProductHandler = (p: Product) => void;
type TodayProductHandler = (p: TodayProduct) => void;

// Product의 공통 속성만 사용하는 함수
const handleProduct: ProductHandler = (p) => {
  console.log(`상품명: ${p.title}, 가격: ${p.price}`);
};

// 반공변성: ProductHandler를 TodayProductHandler에 할당 가능
const handleTodayProduct: TodayProductHandler = handleProduct; // OK!

// TodayProduct를 전달해도 안전
handleTodayProduct({ title: "샴푸", price: 10000, isTodayDelivery: true });
// title과 price는 확실히 존재함

handleProductProduct의 속성만 사용합니다. TodayProductProduct의 모든 속성을 가지고 있으므로, TodayProduct를 전달해도 문제없이 동작합니다. 이것이 매개변수 반공변성이 타입 안전한 이유입니다.

반환 타입은 직관대로 공변적입니다.

type GetProduct = () => Product;
type GetTodayProduct = () => TodayProduct;

const getTodayProduct: GetTodayProduct = () => ({
  title: "샴푸", price: 10000, isTodayDelivery: true
});

// 공변성: GetTodayProduct를 GetProduct에 할당 가능
const getProduct: GetProduct = getTodayProduct; // OK!

const product = getProduct();
console.log(product.title); // TodayProduct도 title이 있으므로 안전

getTodayProduct가 반환하는 TodayProductProduct의 모든 속성을 포함합니다. Product를 기대하는 곳에서 사용해도 안전하죠.

메서드 vs 함수 프로퍼티

함수 타입에서 매개변수는 반공변적이라고 했습니다. 그런데 타입스크립트에서는 같은 함수라도 어떻게 정의하느냐에 따라 이 규칙이 다르게 적용됩니다.

interface Container {
  // 메서드 문법 - 이변성(bivariant)
  process(item: Product): void;

  // 함수 프로퍼티 문법 - strictFunctionTypes에서 반공변성
  handle: (item: Product) => void;
}

메서드는 이변성(bivariance)으로 처리됩니다. 양방향 할당이 모두 허용된다는 뜻입니다. 이것도 실용성을 위한 선택입니다. 기존 자바스크립트 코드와의 호환성, 그리고 일반적인 객체 지향 패턴을 지원하기 위해서입니다.

더 엄격한 타입 검사를 원한다면 메서드 대신 함수 프로퍼티 문법을 사용하세요.


실전 활용 - 유틸리티 타입 이해하기

제네릭의 강력함을 가장 잘 보여주는 것이 타입스크립트의 내장 유틸리티 타입들입니다. 이들이 어떻게 구현되어 있는지 살펴보면 제네릭에 대한 이해가 깊어집니다. (더 자세한 내용은 타입스크립트 공식 핸드북 - Utility Types를 참고하세요.)

Partial<T>

모든 속성을 선택적으로 만드는 유틸리티입니다.

// 구현 원리 - "객체의 모든 키를 순회하며 새 타입을 만든다" 정도로 이해하면 됩니다.
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// 사용 예시 - 올리브영 상품 수정 폼
// (설명을 위해 간략화된 Product 타입)
interface Product {
  title: string;
  price: number;
  description: string;
}

// 수정 시에는 일부 필드만 보내도 됨
type ProductUpdateInput = Partial<Product>;
// { title?: string; price?: number; description?: string; }

function updateProduct(id: string, updates: Partial<Product>) {
  // 전달된 필드만 업데이트
}

updateProduct("123", { price: 25000 }); // OK - price만 업데이트

Pick<T, K>와 Omit<T, K>

특정 속성만 선택하거나 제외합니다.

// 상품 목록에서는 일부 정보만 보여줌
type ProductListItem = Pick<Product, "title" | "price">;
// { title: string; price: number; }

// 상품 생성 시에는 id를 제외
type ProductCreateInput = Omit<Product, "id">;

올리브영 맞춤 유틸리티 타입

내장 유틸리티를 참고하여 올리브영에 필요한 유틸리티 타입도 만들 수 있습니다.

// 할인 적용 결과 타입
type Discounted<T extends HasPrice> = T & {
  originalPrice: number;
  discountRate: number;
};

// 사용
const discountedProduct: Discounted<Product> = {
  ...product,
  originalPrice: product.price,
  discountRate: 15
};


// API 응답에서 데이터만 추출
type ExtractData<T> = T extends ApiResponse<infer U> ? U : never;

type ProductData = ExtractData<ApiResponse<Product>>; // Product

ExtractData 타입에서 사용된 infer 키워드는 조건부 타입에서 타입을 추론하는 데 사용됩니다. 지금은 "이런 것도 가능하구나" 정도로 넘어가도 괜찮습니다. 조건부 타입과 infer는 다음 글에서 더 자세히 다루겠습니다.


마치며

이번 글에서 우리는 제네릭과 매개변수에 의한 다형성을 살펴보았습니다.

서브타입에 의한 다형성은 "A는 B이다"라는 관계로 타입 간 호환성을 만들었습니다. TodayProductProduct이므로 Product가 필요한 곳에 TodayProduct를 쓸 수 있었죠.

매개변수에 의한 다형성은 타입 자체를 추상화합니다. T라는 타입 변수를 사용하여, 어떤 타입이든 받을 수 있는 범용적인 코드를 작성합니다. 서브타입 관계 없이도 같은 로직을 여러 타입에 적용할 수 있게 되었습니다.

그리고 변성(Variance)을 통해 두 다형성이 만났을 때 어떤 일이 일어나는지 이해했습니다. 타입 인자의 서브타입 관계가 제네릭 타입에 어떻게 전파되는지, 그리고 타입스크립트가 실용성과 타입 안전성 사이에서 어떤 선택을 했는지 살펴보았습니다.

올리브영에서도 제네릭은 코드 품질 향상에 크게 기여하고 있습니다. ApiResponse<T>로 수십 개의 API 응답 타입을 하나로 통합했고, 상품·리뷰·주문 등 서로 다른 도메인의 목록을 처리하는 공통 유틸리티 함수들도 제네릭으로 구현되어 있습니다. 덕분에 새로운 도메인이 추가되어도 기존 유틸리티를 그대로 재사용할 수 있고, 타입 안전성은 그대로 유지됩니다.

다음 글에서는 조건부 타입(Conditional Types)과 타입 추론의 심화 내용을 다루어볼 예정입니다. 타입 수준에서 조건을 분기하고, 복잡한 타입을 변환하는 방법을 알아보겠습니다.


Reference

타입스크립트제네릭다형성
올리브영 테크 블로그 작성 올리브영 타입스크립트로 알아보는 제네릭과 매개변수 다형성
🙏
재미 |
Front-end Engineer
자신의 작업물이 세상에 선한 영향력을 미치길 바라는 소프트웨어 개발자입니다.