[TYPESCRIPT] 고급 제네릭 패턴: Higher-Order Type과 Compose Type을 사용하는 이유

 

TypeScript를 어느 정도 사용하다 보면, 단순한 제네릭<T>만으로는 설명되지 않는 순간을 만나게 됩니다.

 

실무에서 흔히 겪는 상황은 다음과 같습니다.

  • 비슷한 타입 정의가 반복되는데, 미묘하게 다르다
  • 타입을 재사용하려고 했는데 오히려 읽기 어려워진다
  • 타입 안정성을 높이려다 제네릭이 과도하게 복잡해진다

 

이 지점에서 많은 개발자가 선택합니다. “여기까지만 타입 쓰자”, 혹은 “any로 타협하자”.

 

하지만 실제로는, 타입을 더 단순하게 만들기 위해 고급 제네릭 패턴이 필요한 순간이 있습니다. 이번 글에서는 Higher-Order Type과 Compose Type을 중심으로, 왜 이런 패턴이 실무에서 의미를 갖는지 차분하게 정리합니다.

 

개념/배경 설명

고급 제네릭 패턴이라는 말은 어렵게 들리지만, 핵심은 단순합니다.

  • 타입을 값처럼 다루고 싶다
  • 타입을 조합해서 새로운 타입을 만들고 싶다
  • 중복 없이 규칙을 표현하고 싶다

 

문제는 이런 요구를 일반 제네릭으로만 풀려고 할 때 발생합니다.

  • 제네릭 인자가 너무 많아진다
  • 타입 정의를 읽는 데 시간이 오래 걸린다
  • 의미보다 구현이 앞서 보인다

 

Higher-Order Type과 Compose Type은 이 문제를 “확장”이 아니라 “구성”으로 해결하려는 시도입니다.

 

설계 포인트 1: Higher-Order Type이 필요한 이유

Higher-Order Type은 타입을 입력으로 받아 새로운 타입을 반환하는 타입입니다. 함수의 고차 함수 개념과 거의 같습니다.

 


// 기본 데이터 타입
type User = {
  id: string;
  name: string;
};

 

여기에 “읽기 전용”, “nullable”, “응답 래핑” 같은 규칙을 여러 곳에서 반복해서 적용한다고 가정해보겠습니다.

 


// Higher-Order Type
type ReadonlyType<T> = {
  readonly [K in keyof T]: T[K];
};

 

이 타입은 값을 모르고도, “어떤 타입이든 읽기 전용으로 바꾼다”는 규칙을 담고 있습니다.

 


type ReadonlyUser = ReadonlyType<User>;

 

실무 포인트 정리

  • Higher-Order Type은 규칙을 캡슐화한다
  • 타입 중복을 줄이기 위해 사용한다
  • 비즈니스 의미보다 “변환 규칙”을 표현할 때 적합하다

 

설계 포인트2: Higher-Order Type을 남용하면 생기는 문제

Higher-Order Type은 강력하지만, 남용하면 오히려 읽기 어려워집니다.

 

  • 타입 정의를 따라가기가 힘들다
  • 에러 메시지가 지나치게 복잡해진다
  • 팀원 간 이해 격차가 커진다

 

실무 기준에서 권장되는 사용 범위는 명확합니다.

  • 2~3단계 이내의 변환
  • 공통 규칙이 반복될 때만 도입
  • 도메인 타입 자체를 감싸지 않는다

 

설계 포인트3: Compose Type으로 타입을 쌓는다

Compose Type은 여러 타입 변환을 순서대로 조합하는 패턴입니다.

 


type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

type ApiResponse<T> = {
  ok: true;
  data: T;
};

 

이제 이 규칙들을 조합해봅니다.

 


type NullableReadonlyApi<T> =
  ApiResponse<ReadonlyType<Nullable<T>>>;

 

이 타입은 이름만 봐도 의도가 드러납니다.

  • nullable
  • readonly
  • api response

 

핵심은 “한 번에 이해할 수 있는 이름과 깊이”를 유지하는 것입니다.

 

실무 포인트 정리

  • Compose Type은 순서를 가진다
  • 타입 이름이 곧 문서가 되도록 짓는다
  • 한 줄에 담을 수 없으면 쪼갠다

 

코드 예제: 실무에서 바로 쓰는 패턴

다음은 API 응답 타입을 구성하는 실제 패턴 예시입니다.

 


type WithMeta<T> = T & {
  meta: {
    requestId: string;
  };
};

type SecureResponse<T> =
  ApiResponse<ReadonlyType<WithMeta<T>>>;

 

이 구조의 장점은 명확합니다.

  • 응답 규칙이 한 곳에 모인다
  • 도메인 타입은 순수하게 유지된다
  • 타입 변경 시 영향 범위를 예측할 수 있다

 

운영/실무에서 자주 겪는 문제

  • 고급 제네릭을 써서 타입 에러가 읽히지 않는 경우
  • 타입 정의가 로직보다 더 복잡해진 경우
  • 한 사람만 이해하는 타입 구조가 된 경우
  • 규칙이 아닌 “기교”를 위한 제네릭 사용

 

이런 문제는 대부분 “왜 쓰는지”보다 “어떻게 쓰는지”에 집중했을 때 발생합니다.

 

실무 권장 체크리스트

  • 이 제네릭 패턴이 중복을 실제로 줄이고 있는가
  • 타입 이름만 보고 의도를 이해할 수 있는가
  • 에러 메시지가 감당 가능한 수준인가
  • 2~3단계 이상 중첩되지 않았는가
  • 팀 내에서 공유 가능한 패턴인가

 


 

 

고급 제네릭 패턴의 목적은 타입을 똑똑하게 만드는 것이 아닙니다.

타입을 통해 규칙을 드러내고, 중복을 줄이며, 실수를 구조적으로 막는 것 입니다. Higher-Order Type과 Compose Type은 그 목적이 분명할 때만 사용해야 가치가 있습니다.