프로젝트가 커지면 TypeScript 타입 체크 시간이 급증할 수 있습니다. --generateTrace로 병목을 찾고 타입 설계를 개선해서 성능을 최적화할 수 있습니다.

성능 측정

tsc --generateTrace

BASH
# 타입 체크 트레이스 생성
npx tsc --generateTrace ./trace-output

# 결과 확인
# trace-output/trace.json → Chrome DevTools의 Performance 탭에서 열기
# trace-output/types.json → 타입 체크에 시간이 걸린 타입 목록

Chrome DevTools의 Performance 탭에서 trace.json을 열면 어떤 파일, 어떤 타입에서 시간이 걸리는지 시각적으로 확인할 수 있습니다.

--extendedDiagnostics

BASH
npx tsc --extendedDiagnostics

# 출력 예시:
# Files:               1234
# Lines:               98765
# Parse time:          1.23s
# Bind time:           0.45s
# Check time:          4.67s   ← 타입 체크 시간
# Emit time:           0.89s
# Total time:          7.24s

느린 타입의 원인

1. 복잡한 조건부 타입

TYPESCRIPT
// ❌ 느림 — 깊은 재귀
type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

// ✅ 개선 — 깊이 제한
type DeepPartial2<T, Depth extends number = 5> = Depth extends 0
  ? T
  : T extends object
    ? { [K in keyof T]?: DeepPartial2<T[K], Subtract<Depth, 1>> }
    : T;

2. 대규모 유니온 타입

TYPESCRIPT
// ❌ 느림 — 수천 개의 유니온 멤버
type AllIcons = 'icon-1' | 'icon-2' | /* ... 5000개 ... */ | 'icon-5000';

// 조건부 타입에 대규모 유니온이 들어가면 각 멤버에 분배됨
type Boxed = Box<AllIcons>; // 5000번 계산

3. 인터섹션 vs interface extends

TYPESCRIPT
// ❌ 느림 — 인터섹션은 매번 평탄화
type User = Base & PersonalInfo & ContactInfo & Permissions & Settings;

// ✅ 빠름 — interface extends는 캐싱됨
interface User extends Base, PersonalInfo, ContactInfo, Permissions, Settings {}

공부하다 보니 TypeScript 컴파일러 팀도 "가능하면 인터섹션 대신 interface extends를 쓰라"고 권장하더라고요.

4. 과도한 타입 인스턴스화

TYPESCRIPT
// ❌ 느림 — 제네릭이 많은 조합으로 인스턴스화됨
type ComplexGeneric<A, B, C, D, E> = /* 복잡한 타입 계산 */;

// 수많은 조합으로 사용되면 각각 계산해야 함

최적화 기법

skipLibCheck

JSON
{
  "compilerOptions": {
    "skipLibCheck": true  // .d.ts 파일의 타입 검사를 건너뜀
  }
}

이 옵션 하나로 빌드 시간이 상당히 줄어들 수 있습니다. node_modules의 타입을 검사하지 않으므로 안전합니다.

증분 빌드 (Incremental)

JSON
{
  "compilerOptions": {
    "incremental": true,        // 증분 빌드 활성화
    "tsBuildInfoFile": "./.tsbuildinfo" // 빌드 정보 파일
  }
}

이전 빌드 정보를 캐싱해서 변경된 파일만 다시 검사합니다.

프로젝트 레퍼런스

큰 프로젝트를 하위 프로젝트로 분리하면 각 부분을 독립적으로 캐싱합니다.

JSON
{
  "references": [
    { "path": "./packages/shared" },
    { "path": "./packages/api" },
    { "path": "./packages/web" }
  ]
}

타입 설계 개선

TYPESCRIPT
// ❌ 복잡한 Mapped Type이 반복 사용
type FormState<T> = {
  [K in keyof T]: {
    value: T[K];
    error: string | null;
    touched: boolean;
    dirty: boolean;
  };
};

// ✅ 결과를 캐싱 (type alias는 한 번만 계산)
type FieldState<T> = {
  value: T;
  error: string | null;
  touched: boolean;
  dirty: boolean;
};

type FormState<T> = {
  [K in keyof T]: FieldState<T[K]>;
};

배럴 파일 최소화

TYPESCRIPT
// ❌ 거대한 배럴 파일 — 하나만 import해도 전체 타입 로딩
export * from './user';
export * from './product';
export * from './order';
// ... 수백 개

// ✅ 직접 import
import { User } from './models/user';

성능 체크리스트

항목확인
skipLibCheck: true거의 항상 켜야 함
incremental: true개발 시 필수
interface vs 인터섹션가능하면 interface extends 사용
배럴 파일 크기작게 유지
제네릭 재귀 깊이제한 설정
유니온 멤버 수수백 개 이하 유지
프로젝트 레퍼런스대규모 프로젝트에서 사용

정리

  • --generateTrace로 타입 체크 병목을 시각적으로 분석할 수 있다
  • skipLibCheck, incremental은 거의 항상 켜야 한다
  • interface extends가 인터섹션(&)보다 타입 체크 성능이 좋다
  • 깊은 재귀 타입과 대규모 유니온은 성능 저하의 주요 원인이다
  • 프로젝트 레퍼런스로 대규모 프로젝트를 분리하면 증분 빌드가 가능하다
댓글 로딩 중...