성능 — 타입 체크 속도 최적화, tsc --generateTrace 분석
프로젝트가 커지면 TypeScript 타입 체크 시간이 급증할 수 있습니다.
--generateTrace로 병목을 찾고 타입 설계를 개선해서 성능을 최적화할 수 있습니다.
성능 측정
tsc --generateTrace
# 타입 체크 트레이스 생성
npx tsc --generateTrace ./trace-output
# 결과 확인
# trace-output/trace.json → Chrome DevTools의 Performance 탭에서 열기
# trace-output/types.json → 타입 체크에 시간이 걸린 타입 목록
Chrome DevTools의 Performance 탭에서 trace.json을 열면 어떤 파일, 어떤 타입에서 시간이 걸리는지 시각적으로 확인할 수 있습니다.
--extendedDiagnostics
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. 복잡한 조건부 타입
// ❌ 느림 — 깊은 재귀
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. 대규모 유니온 타입
// ❌ 느림 — 수천 개의 유니온 멤버
type AllIcons = 'icon-1' | 'icon-2' | /* ... 5000개 ... */ | 'icon-5000';
// 조건부 타입에 대규모 유니온이 들어가면 각 멤버에 분배됨
type Boxed = Box<AllIcons>; // 5000번 계산
3. 인터섹션 vs interface extends
// ❌ 느림 — 인터섹션은 매번 평탄화
type User = Base & PersonalInfo & ContactInfo & Permissions & Settings;
// ✅ 빠름 — interface extends는 캐싱됨
interface User extends Base, PersonalInfo, ContactInfo, Permissions, Settings {}
공부하다 보니 TypeScript 컴파일러 팀도 "가능하면 인터섹션 대신 interface extends를 쓰라"고 권장하더라고요.
4. 과도한 타입 인스턴스화
// ❌ 느림 — 제네릭이 많은 조합으로 인스턴스화됨
type ComplexGeneric<A, B, C, D, E> = /* 복잡한 타입 계산 */;
// 수많은 조합으로 사용되면 각각 계산해야 함
최적화 기법
skipLibCheck
{
"compilerOptions": {
"skipLibCheck": true // .d.ts 파일의 타입 검사를 건너뜀
}
}
이 옵션 하나로 빌드 시간이 상당히 줄어들 수 있습니다. node_modules의 타입을 검사하지 않으므로 안전합니다.
증분 빌드 (Incremental)
{
"compilerOptions": {
"incremental": true, // 증분 빌드 활성화
"tsBuildInfoFile": "./.tsbuildinfo" // 빌드 정보 파일
}
}
이전 빌드 정보를 캐싱해서 변경된 파일만 다시 검사합니다.
프로젝트 레퍼런스
큰 프로젝트를 하위 프로젝트로 분리하면 각 부분을 독립적으로 캐싱합니다.
{
"references": [
{ "path": "./packages/shared" },
{ "path": "./packages/api" },
{ "path": "./packages/web" }
]
}
타입 설계 개선
// ❌ 복잡한 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]>;
};
배럴 파일 최소화
// ❌ 거대한 배럴 파일 — 하나만 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가 인터섹션(
&)보다 타입 체크 성능이 좋다 - 깊은 재귀 타입과 대규모 유니온은 성능 저하의 주요 원인이다
- 프로젝트 레퍼런스로 대규모 프로젝트를 분리하면 증분 빌드가 가능하다
댓글 로딩 중...