안동민 개발노트

본문 시작

증분 컴파일과 빌드 최적화

incremental 캐시와 watch 모드, 프로젝트 레퍼런스 빌드를 활용해 변경된 TypeScript 코드만 다시 검사·출력합니다.

대규모 타입스크립트 프로젝트에서는 코드 양이 늘어나면서 전체 컴파일 시간이 길어지기 쉽습니다.

코드를 조금 수정할 때마다 빌드를 오래 기다리면 생산성은 급격히 떨어집니다.

증분 컴파일(Incremental Compilation)과 다양한 빌드 최적화 기법은 이 문제를 줄이고 개발 워크플로우를 가속화하는 데 목적이 있습니다.


증분 컴파일

증분 컴파일(Incremental Compilation)은 TypeScript 3.4부터 도입된 기능으로, 이전 컴파일에서 변경된 파일과 그에 영향을 받는 파일만 다시 컴파일하는 방식입니다.

이를 통해 전체 프로젝트를 처음부터 다시 컴파일하는 것보다 훨씬 빠르게 빌드를 완료할 수 있습니다.

증분 컴파일을 활성화하려면 tsconfig.json 파일에 다음 옵션을 추가해야 합니다.

tsconfig.json
{
  "compilerOptions": {
    "incremental": true, // 증분 컴파일 활성화
    "tsBuildInfoFile": "./.tsbuildinfo", // 증분 빌드 정보를 저장할 파일 경로 (선택 사항)
    // ... 다른 컴파일러 옵션 ...
  },
  // ...
}
  • "incremental": true: 이 옵션을 true로 설정하면 TypeScript 컴파일러는 이전에 컴파일된 정보를 기반으로 변경 파일과 영향을 받는 작업을 판별하고 재사용 가능한 결과를 활용합니다.
  • "tsBuildInfoFile" (선택 사항): 컴파일러는 증분 빌드에 필요한 정보를 tsbuildinfo 파일에 저장합니다. 기본 경로는 outFile, rootDir, outDir와 설정 파일 위치에 따라 달라지므로, 고정된 위치가 필요하면 이 옵션으로 지정합니다. 일반적으로 이 파일은 .gitignore에 추가해 버전 관리에서 제외합니다.
증분 컴파일의 원리

첫 번째 컴파일 시, TypeScript는 모든 소스 파일을 컴파일하고, tsbuildinfo 파일에 각 파일의 해시(Hash), 의존성 그래프, 타입 정보 등 증분 빌드에 필요한 메타데이터를 저장합니다.

이후 컴파일 시, TypeScript는 소스 파일의 변경 사항을 감지합니다.

tsbuildinfo 파일에 저장된 정보와 비교하여 실제로 변경되거나 변경된 파일에 의해 영향을 받는 파일들만 식별합니다.

식별된 파일들만 다시 컴파일하고, 변경된 정보를 tsbuildinfo 파일에 업데이트합니다.

incremental은 별도 실행 사이에 디스크의 빌드 정보를 재사용합니다. watch는 같은 프로세스에서 변경을 감시하고 상태를 재사용하는 기능입니다. 둘은 관련 있지만 동일한 설정은 아니며, 반응 시간은 실제 프로젝트에서 측정해야 합니다.


tsc --watch (Watch Mode)

개발 과정에서 코드를 수정할 때마다 수동으로 tsc 명령어를 실행하는 것은 번거롭습니다.

tsc --watch (또는 tsc -w) 명령어는 파일 시스템 변경을 감지하여 변경된 파일만 자동으로 증분 컴파일하도록 합니다.

tsc --watch

이 명령어는 터미널에서 계속 실행되며, 파일을 저장할 때마다 자동으로 다시 컴파일 과정을 시작합니다.

이는 개발 워크플로우에서 매우 중요한 기능입니다.


프로젝트 레퍼런스와 증분 빌드

아래 다이어그램은 증분 빌드가 파일 변경, 의존성 그래프, tsbuildinfo를 기준으로 어떤 프로젝트를 다시 빌드할지 판단하는 흐름을 보여줍니다.

증분 빌드는 프로그램 그래프를 갱신하는 운영 루프다

INCREMENTAL · CACHE LOOP

증분 빌드는 프로그램 그래프를 갱신하는 운영 루프다

변화를 감지하고 이전 그래프와 비교해 영향을 무효화한 뒤 검사·emit 결과를 기록하고 다음 변경을 기다린다.

증분 빌드는 프로그램 그래프를 갱신하는 운영 루프다 변화를 감지하고 이전 그래프와 비교해 영향을 무효화한 뒤 검사·emit 결과를 기록하고 다음 변경을 기다린다. 감지 파일 변화 비교 이전 서명 무효화 영향 범위 검사·emit 필요 작업 기록 새 서명 program graph tsbuildinfo
  1. 파일 변화를 감지한다.

    파일 변화를 감지한다.

  2. 이전 프로그램의 서명과

    이전 프로그램의 서명과 비교한다.

  3. 영향받는 파일과 프로젝트만

    영향받는 파일과 프로젝트만 무효화한다.

  4. 필요한 검사와 emit을

    필요한 검사와 emit을 수행한다.

  5. 새 상태를 .tsbuildinfo에

    새 상태를 .tsbuildinfo에 기록한다.

캐시는 정답의 대체물이 아니라 같은 입력에서 작업을 줄이는 재사용 가능한 상태다.

9장 3절에서 다룬 프로젝트 레퍼런스(Project References)는 증분 컴파일을 여러 하위 프로젝트(모듈)에 걸쳐 확장하는 개념입니다.

tsconfig.json 파일에 composite: true를 설정한 하위 프로젝트들을 references로 연결하고, 최상위 tsconfig.json에서 이들을 참조합니다.

이 경우, 단순한 tsc 대신 tsc --build (또는 tsc -b) 명령어를 사용하여 증분 빌드를 수행합니다.

tsc --build

tsc --build는 다음과 같은 방식으로 증분 빌드를 최적화합니다.

  • 의존성 순서 파악: references에 정의된 프로젝트 간의 의존성 그래프를 분석하여 올바른 빌드 순서를 자동으로 결정합니다.
  • 하위 프로젝트별 증분 빌드: 각 하위 프로젝트에 대해 tsbuildinfo 파일을 사용하여 증분 컴파일을 수행합니다.
  • 변경된 프로젝트만 빌드: 하위 프로젝트의 소스 코드가 변경되었거나, 해당 프로젝트가 의존하는 프로젝트가 변경되었을 때만 해당 하위 프로젝트를 다시 빌드합니다. 예를 들어 common의 공개 선언 변화는 ui에 영향을 줄 수 있습니다. 내부 구현만 바뀌고 선언이 같으면 소비자의 일부 작업을 재사용할 수 있으며, 옵션·산출물·캐시 상태도 빌드 판정에 영향을 줍니다.

이는 모노레포와 같은 대규모 멀티-패키지 프로젝트에서 빌드 시간을 획기적으로 줄이는 데 핵심적인 역할을 합니다.


기타 빌드 최적화 기법

증분 컴파일 외에도 타입스크립트 빌드 시간을 최적화하는 데 도움이 되는 몇 가지 방법들이 있습니다.

적절한 tsconfig.json 설정
  • skipLibCheck: true: 선언 파일 자체의 검사를 건너뜁니다. node_modules에만 한정되지 않으며, 중복되거나 충돌하는 선언의 오류를 가릴 수 있으므로 검사 비용과 정확성의 절충을 판단합니다. 애플리케이션 코드가 그 타입을 사용하는 검사는 계속됩니다.
  • isolatedModules: true: 각 파일이 독립적인 모듈임을 확인하여 단일 파일 트랜스파일러(예: Babel, esbuild)와의 호환성을 높입니다. 이는 트랜스파일러가 .ts 파일을 개별적으로 처리할 수 있게 하여 빌드 파이프라인의 속도를 높일 수 있습니다.
  • noEmit: true: 컴파일러가 JS 파일을 생성하지 않고 오직 타입 검사만 수행하도록 합니다. 최종 JS 파일 생성은 Webpack, Vite, esbuild와 같은 번들러에게 맡길 때 사용합니다. 타입 검사는 TypeScript가, 실제 트랜스파일은 더 빠른 다른 도구가 하는 방식으로 역할을 분담하여 빌드를 가속화합니다.

더 빠른 트랜스파일러/번들러 사용: TypeScript 컴파일러(tsc)는 타입 검사도 함께 수행하기 때문에 순수한 트랜스파일러(JS 변환기)보다 느릴 수 있습니다.

빌드 속도가 중요하다면, 타입 검사는 TypeScript에 맡기고 실제 .ts -> .js 변환은 더 빠른 도구를 사용하는 파이프라인을 고려할 수 있습니다.

  • esbuild: 매우 빠른 속도로 유명한 빌드 도구입니다. 타입스크립트 코드를 JavaScript로 트랜스파일하는 데 tsc보다 훨씬 빠릅니다.
  • SWC: Rust로 작성된 또 다른 빠른 트랜스파일러입니다. Next.js, Parcel 등 많은 프로젝트에서 사용됩니다.
  • Babel: @babel/preset-typescript를 사용하여 TypeScript 코드를 트랜스파일할 수 있습니다. Babel은 타입 검사를 수행하지 않으므로, 타입 검사를 위해서는 여전히 tsc를 별도로 실행해야 합니다.
  • Vite: 변환·번들링 도구 구성은 Vite 버전에 따라 다릅니다. TS 변환과 타입 검사는 별도 책임이며, tsc 검사 프로세스나 CI 단계를 프로젝트에서 직접 구성해야 합니다.

이러한 도구들은 tsc가 타입 검사만 하도록 noEmit: true를 설정하고, 실제 JS 변환 및 번들링은 이 도구들이 담당하게 합니다.

타입 가져오기 전략
  • 필요한 타입만 임포트: 사용하지 않는 모듈이나 객체를 임포트하지 않도록 주의합니다.
  • types 필드 활용: tsconfig.json의 types 필드를 사용하여 전역으로 포함할 @types 패키지를 명시적으로 제한할 수 있습니다. 이는 불필요한 타입 정의 로드를 방지합니다.
하드웨어 리소스
  • 더 빠른 CPU, 충분한 RAM, NVMe SSD 등 하드웨어 사양을 개선하는 것도 빌드 속도에 직접적인 영향을 미칩니다.

증분 컴파일과 다양한 빌드 최적화 기법은 대규모 타입스크립트 프로젝트의 개발 경험을 크게 좌우합니다.

incremental: true와 tsc --build로 증분 빌드를 활성화하고, 필요에 따라 skipLibCheck, isolatedModules, noEmit 같은 컴파일러 옵션을 조정하며, 더 빠른 번들러/트랜스파일러를 활용하는 것은 생산성 극대화에 중요합니다.

이러한 최적화 전략을 적절히 조합해 프로젝트 빌드 시간을 효과적으로 관리하세요.

빌드가 느려졌을 때는 도구 이름보다 먼저 타입 검사, 변환, 번들링, 테스트 중 어느 단계가 병목인지 분리해야 합니다.

최적화 설정은 로컬과 CI에서 같은 기준으로 검증되어야 합니다.

아래는 최적화를 평가하는 측정 절차이며, 이 절에서 수행한 벤치마크 결과를 뜻하지 않습니다.

빌드 최적화는 cold 기준선부터 측정한다

MEASURE · OPTIMIZE

빌드 최적화는 cold 기준선부터 측정한다

type-check·transpile·bundle·test 시간을 분리하고 cold와 warm, 로컬과 CI의 차이를 확인한 뒤 병목에만 최적화를 적용한다.

빌드 최적화는 cold 기준선부터 측정한다 type-check·transpile·bundle·test 시간을 분리하고 cold와 warm, 로컬과 CI의 차이를 확인한 뒤 병목에만 최적화를 적용한다. 아니오 예 cold 기준선 단계별 시간 기록 병목이 재현되는가? CI와 로컬 비교 측정부터 교정 캐시 hit 숨김 제거 영향 범위 최적화 references · cache
  1. cold run에서 단계별

    cold run에서 단계별 기준선을 기록한다.

  2. warm run과 CI에서

    warm run과 CI에서 동일 병목이 반복되는지 본다.

  3. type-check와 transpile을 같은

    type-check와 transpile을 같은 비용으로 취급하지 않는다.

  4. affected scope와 cache

    affected scope와 cache hit를 검증한다.

  5. 변경 전후 결과와

    변경 전후 결과와 시간을 함께 비교한다.

빠른 한 번보다 재현 가능한 cold/warm 측정이 최적화의 근거다.