[TYPESCRIPT] 타입 추론이 과하게 깊어질 때 생기는 성능 문제와 완화 방법

코드가 커질수록 TypeScript의 타입 추론은 점점 많은 일을 하게 됩니다. 처음에는 편리했던 제네릭과 조건부 타입이, 어느 시점부터는 IDE가 멈칫거리거나 컴파일 시간이 눈에 띄게 늘어나는 원인이 되기도 합니다.

 

이 문제는 문법 오류처럼 바로 드러나지 않습니다. 기능은 정상 동작하지만, 개발 경험이 점점 무거워집니다. 온보딩 환경에서만 느리거나, 특정 파일을 열 때만 타입 분석이 오래 걸리는 식으로 나타나는 경우도 많습니다.

 

타입 추론이 깊어질 때 실제로 어떤 일이 벌어지는지, 어떤 패턴에서 비용이 커지는지, 그리고 구조를 크게 바꾸지 않고 완화하는 방법을 알아보겠습니다.

 

타입 추론 비용이 커지는 이유

TypeScript는 타입을 계산할 때 추론 그래프를 계속 확장합니다. 제네릭, 분배 조건부 타입, 매핑 타입이 겹치면, 컴파일러는 가능한 경우의 수를 따라가며 타입을 계산합니다.

 

간단한 코드에서는 문제가 없지만, 다음 조건이 겹치면 비용이 급격히 증가합니다.

 

  • 제네릭 중첩 깊이가 깊은 경우
  • 분배 조건부 타입이 유니온에 반복 적용되는 경우
  • 재귀 타입이 넓은 객체에 적용되는 경우
  • infer가 여러 단계로 이어지는 경우

 

이 상태가 되면 타입 시스템이 코드 보조 도구를 넘어, 컴파일 병목 지점이 되기 시작합니다.

 

실제로 많이 보게 되는 패턴

과도하게 일반화된 유틸 타입


type DeepReadonly<T> =
  T extends (...args: any[]) => any
    ? T
    : T extends object
      ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
      : T;

 

이런 유틸은 강력하지만, 넓은 객체 트리에 적용되면 타입 계산 깊이가 빠르게 증가합니다. 특히 API 응답처럼 필드가 많은 구조에 반복 적용될 때 비용이 눈에 띄게 올라갑니다.

 

분배 조건부 타입이 유니온에 중첩되는 경우


type Normalize<T> = T extends any
  ? { value: T }
  : never;

 

유니온 크기가 커질수록 분배 횟수도 함께 늘어납니다. 여기에 또 다른 조건부 타입이 얹히면, 타입 계산 트리가 빠르게 커집니다.

 

IDE에서 먼저 나타나는 신호

컴파일 시간이 눈에 띄게 늘기 전에, 에디터에서 다음 증상이 먼저 나타나는 경우가 많습니다.

 

  • 자동완성이 늦게 뜸
  • 타입 hover 표시가 지연됨
  • 특정 파일 열 때 CPU 사용량 증가
  • 타입 에러 표시가 늦게 반영됨

 

이 단계에서 구조를 한 번 정리해 두면, 빌드 병목으로 번지는 것을 막을 수 있습니다.

 

완화 방법 1: 경계에서 타입 계산을 멈추기

복잡한 타입 계산이 필요한 경우라도, 그 결과를 다시 계산에 넘기지 않는 것이 도움이 됩니다.

 


// before
type Result<T> = DeepReadonly<Normalize<T>>;

// after
type Normalized<T> = Normalize<T>;
type Result<T> = DeepReadonly<Normalized<T>>;

 

중간 타입을 명시적으로 끊어주면, 컴파일러가 재추론해야 하는 범위가 줄어듭니다. 코드 가독성도 함께 좋아지는 경우가 많습니다.

 

완화 방법 2: 재귀 타입의 적용 범위를 제한하기

Deep 계열 유틸은 범위를 명확히 두는 편이 안전합니다.

 


type ShallowReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

 

전체 트리를 재귀로 감싸기보다, 필요한 레이어까지만 적용하는 방식이 유지 비용을 낮춥니다.

 

완화 방법 3: 반환 타입을 명시적으로 고정하기

추론에 전적으로 맡기면, 제네릭 체인이 길어질수록 타입 계산 비용도 함께 증가합니다.

 


// before
export const createUser = (input: CreateUserInput) => {
  return {
    id: generateId(),
    ...input,
  };
};

// after
export type CreateUserResult = {
  id: string;
  name: string;
};

export const createUser = (input: CreateUserInput): CreateUserResult => {
  return {
    id: generateId(),
    name: input.name,
  };
};

 

반환 타입을 고정해 두면, 호출 지점에서의 추론 비용이 줄어듭니다.

 

완화 방법 4: 타입 퍼즐은 경계 레이어에만 두기

조건부 타입이나 복잡한 제네릭은 프레임워크 어댑터나 API 변환 계층에 두고, 도메인 내부는 명시적 타입으로 유지하는 편이 안정적입니다.

 

도메인 로직은 수정 빈도가 높고, 여러 사람이 동시에 만지는 영역이기 때문에, 타입 계산 복잡도가 바로 유지 비용으로 이어집니다.

 

운영 중 체감되는 영향

타입 추론 비용이 커지면 다음 형태로 체감되는 경우가 많습니다.

 

  • CI 타입 체크 시간이 계속 증가
  • 증분 빌드 이점이 줄어듦
  • IDE 메모리 사용량 증가
  • 대규모 PR에서 타입 체크 병목 발생

 

특히 모노레포에서는 한 패키지의 타입 복잡도가 다른 패키지 개발 경험까지 함께 느리게 만드는 경우가 있습니다.

 

점검할 때 보게 되는 항목들

  • 분배 조건부 타입이 넓은 유니온에 반복 적용되고 있는지
  • 재귀 타입이 API 응답 전체에 적용되고 있지 않은지
  • infer 체인이 여러 단계로 이어져 있는지
  • 반환 타입이 추론에 과도하게 의존하고 있는지
  • 타입 hover 표시가 유난히 느린 파일이 존재하는지
  • 타입 체크 시간이 최근에 눈에 띄게 증가했는지

 


 

 

TypeScript의 타입 시스템은 강력하지만, 모든 정밀함이 항상 이득으로 이어지는 것은 아닙니다. 추론 깊이가 커질수록, 컴파일과 개발 경험에 비용이 누적됩니다.