증분 컴파일과 빌드 최적화
incremental 캐시와 watch 모드, 프로젝트 레퍼런스 빌드를 활용해 변경된 TypeScript 코드만 다시 검사·출력합니다.
대규모 타입스크립트 프로젝트에서는 코드의 양이 늘어나면서 전체 컴파일 시간이 길어지기 쉽습니다.
코드를 조금 수정할 때마다 빌드를 오래 기다리면 생산성은 급격히 떨어집니다.
증분 컴파일(Incremental Compilation)과 다양한 빌드 최적화 기법은 이 문제를 줄이고 개발 워크플로우를 가속화하는 데 목적이 있습니다.
증분 컴파일
앞선 절에서 준비할 것과 캐시의 역할
이 절은 tsc -p로 한 프로젝트를 검사하는 방법과 references·composite로 여러 프로젝트를 연결하는 방법을 전제로 합니다. 여기서 캐시는 이전 작업을 재사용하는 빌드 정보이고, 배포 산출물은 실행할 JS나 공개 타입 선언입니다. .tsbuildinfo를 JS에서 import하지 않습니다. 이 파일이 없어지면 다시 생성할 수 있지만, 캐시가 있다는 사실만으로 최근 소스의 타입 검사나 배포가 성공한 것은 아닙니다. incremental 문서의 저장 정보와 런타임 구분을 기준으로 봅니다.
tsc -w는 같은 프로세스를 유지하며 변경에 반응하고, incremental은 별도 실행에서 디스크의 정보를 읽습니다. watch를 종료한 뒤 다시 켜는 경우와 실행 중 파일 하나를 바꾸는 경우를 같은 warm 조건으로 기록하지 마세요.
증분 컴파일(Incremental Compilation)은 TypeScript 3.4부터 도입된 기능으로, 이전 컴파일에서 변경된 파일과 그에 영향을 받는 파일만 다시 컴파일하는 방식입니다.
이를 통해 전체 프로젝트를 처음부터 다시 컴파일하는 것보다 훨씬 빠르게 빌드를 완료할 수 있습니다.
증분 컴파일을 활성화하려면 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를 기준으로 어떤 프로젝트를 다시 빌드할지 판단하는 흐름을 보여줍니다.
증분 빌드는 프로그램 그래프를 갱신하는 운영 루프다 — 상태 추적
이전 서명과 소스/옵션 변화, 필요한 검사/emit 및 .tsbuildinfo 상태가 반복 빌드에서 달라진다. 연결 MDX의 단일 프로젝트 6.0.3 실습으로 cache 존재와 오류/기존 산출물 보존을 구분할 가치가 있다.
CHANGE / GRAPH / REUSE
변경의 영향을 계산하고 필요한 작업 뒤 재사용 상태를 갱신한다
파일·옵션·산출물 상태와 이전 그래프를 비교해 필요한 검사·출력을 고릅니다. watch는 실행 중 상태를, incremental은 실행 사이 저장한 빌드 정보를 재사용합니다.
DETECT
입력과 상태 변화
소스뿐 아니라 옵션·의존성·산출물·cache 상태도 판정에 영향을 줍니다. 같은 입력에서 작업을 재사용할 수 있는지 확인합니다.
AFFECTED
선언과 구현 변화의 영향
공개 선언이 바뀌면 소비자 검사에 영향을 줄 수 있습니다. 내부 구현만 바뀌고 선언이 같다면 소비자의 일부 작업을 재사용할 수 있습니다.
INCREMENTAL
실행 사이 디스크 재사용
incremental은 .tsbuildinfo를 저장합니다. 이 파일은 배포 산출물이 아니며 삭제해도 다시 만들 수 있는 cache입니다.
WATCH / BUILD
실행 중 감시와 프로젝트 빌드
tsc -w는 같은 프로세스에서 변경을 감시합니다. tsc -b는 프로젝트 참조 그래프의 오래된 작업을 의존 순서로 처리합니다.
cache는 정확성의 대체물이 아니라 검증된 입력에서 필요한 작업을 줄이는 상태입니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 원도표 설명 | incremental은 .tsbuildinfo를 저장합니다. 이 파일은 배포 산출물이 아니며 삭제해도 다시 만들 수 있는 cache입니다. |
| 원도표 설명 | tsc -w는 같은 프로세스에서 변경을 감시합니다. tsc -b는 프로젝트 참조 그래프의 오래된 작업을 의존 순서로 처리합니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 성공 total 3 | 진단: 성공 / 산출: dist/index.js | 지정한 6.0.3 구성으로 total:number=3을 검사한다 | incremental cache는 실행 프로그램의 import 대상이 아니다 |
| 캐시만 없음 | cache: 없음 / JS: 기존 파일 유지 | build.tsbuildinfo만 없어져도 기존 JS는 남고 다음 검사에서 cache를 재구성할 수 있다 | cache 부재가 자체 타입 오류를 뜻하지 않는다 |
| 잘못된 문자열 3 | 진단: string→number 오류 / JS: 이전 파일 잔존 | total:number="3"는 진단을 만들고 noEmitOnError:true가 새 JS 방출을 막는다 | cache는 검사 면제권이 아니고 잔존 JS는 새 검사 성공 증거가 아니다 |
| 검사와 emit 정책 | 검사: app 오류는 검사 / emit: noEmit이면 없음 | skipLibCheck로 해당 app 오류는 없어지지 않으며 고친 뒤 noEmit:true도 새 JS는 안 만든다 | cache/진단/새 산출/기존 파일의 네 관찰을 분리한다 |
증분 빌드는 프로그램 그래프를 갱신하는 운영 루프다 — 상태 추적
이전 서명과 소스/옵션 변화, 필요한 검사/emit 및 .tsbuildinfo 상태가 반복 빌드에서 달라진다. 연결 MDX의 단일 프로젝트 6.0.3 실습으로 cache 존재와 오류/기존 산출물 보존을 구분할 가치가 있다.
CHANGE / GRAPH / REUSE
변경의 영향을 계산하고 필요한 작업 뒤 재사용 상태를 갱신한다
파일·옵션·산출물 상태와 이전 그래프를 비교해 필요한 검사·출력을 고릅니다. watch는 실행 중 상태를, incremental은 실행 사이 저장한 빌드 정보를 재사용합니다.
DETECT
입력과 상태 변화
소스뿐 아니라 옵션·의존성·산출물·cache 상태도 판정에 영향을 줍니다. 같은 입력에서 작업을 재사용할 수 있는지 확인합니다.
AFFECTED
선언과 구현 변화의 영향
공개 선언이 바뀌면 소비자 검사에 영향을 줄 수 있습니다. 내부 구현만 바뀌고 선언이 같다면 소비자의 일부 작업을 재사용할 수 있습니다.
INCREMENTAL
실행 사이 디스크 재사용
incremental은 .tsbuildinfo를 저장합니다. 이 파일은 배포 산출물이 아니며 삭제해도 다시 만들 수 있는 cache입니다.
WATCH / BUILD
실행 중 감시와 프로젝트 빌드
tsc -w는 같은 프로세스에서 변경을 감시합니다. tsc -b는 프로젝트 참조 그래프의 오래된 작업을 의존 순서로 처리합니다.
cache는 정확성의 대체물이 아니라 검증된 입력에서 필요한 작업을 줄이는 상태입니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 원도표 설명 | incremental은 .tsbuildinfo를 저장합니다. 이 파일은 배포 산출물이 아니며 삭제해도 다시 만들 수 있는 cache입니다. |
| 원도표 설명 | tsc -w는 같은 프로세스에서 변경을 감시합니다. tsc -b는 프로젝트 참조 그래프의 오래된 작업을 의존 순서로 처리합니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 성공 total 3 | 진단: 성공 / 산출: dist/index.js | 지정한 6.0.3 구성으로 total:number=3을 검사한다 | incremental cache는 실행 프로그램의 import 대상이 아니다 |
| 캐시만 없음 | cache: 없음 / JS: 기존 파일 유지 | build.tsbuildinfo만 없어져도 기존 JS는 남고 다음 검사에서 cache를 재구성할 수 있다 | cache 부재가 자체 타입 오류를 뜻하지 않는다 |
| 잘못된 문자열 3 | 진단: string→number 오류 / JS: 이전 파일 잔존 | total:number="3"는 진단을 만들고 noEmitOnError:true가 새 JS 방출을 막는다 | cache는 검사 면제권이 아니고 잔존 JS는 새 검사 성공 증거가 아니다 |
| 검사와 emit 정책 | 검사: app 오류는 검사 / emit: noEmit이면 없음 | skipLibCheck로 해당 app 오류는 없어지지 않으며 고친 뒤 noEmit:true도 새 JS는 안 만든다 | cache/진단/새 산출/기존 파일의 네 관찰을 분리한다 |
9장 3절에서 다룬 프로젝트 레퍼런스(Project References)는 증분 컴파일을 여러 하위 프로젝트(모듈)에 걸쳐 확장하는 개념입니다.
tsconfig.json 파일에 composite: true를 설정한 하위 프로젝트들을 references로 연결하고, 최상위 tsconfig.json에서 이들을 참조합니다.
이 경우, 단순한 tsc 대신 tsc --build (또는 tsc -b) 명령어를 사용하여 증분 빌드를 수행합니다.
tsc --buildtsc --build는 다음과 같은 방식으로 증분 빌드를 최적화합니다.
- 의존성 순서 파악:
references에 정의된 프로젝트 간의 의존성 그래프를 분석하여 올바른 빌드 순서를 자동으로 결정합니다. - 하위 프로젝트별 증분 빌드: 각 하위 프로젝트에 대해
tsbuildinfo파일을 사용하여 증분 컴파일을 수행합니다. - 변경된 프로젝트만 빌드: 하위 프로젝트의 소스 코드가 변경되었거나, 해당 프로젝트가 의존하는 프로젝트가 변경되었을 때만 해당 하위 프로젝트를 다시 빌드합니다. 예를 들어
common의 공개 선언 변화는ui에 영향을 줄 수 있습니다. 내부 구현만 바뀌고 선언이 같으면 소비자의 일부 작업을 재사용할 수 있으며, 옵션·산출물·캐시 상태도 빌드 판정에 영향을 줍니다.
이는 모노레포와 같은 대규모 멀티-패키지 프로젝트에서 빌드 시간을 획기적으로 줄이는 데 핵심적인 역할을 합니다.
기타 빌드 최적화 기법
증분 컴파일 외에도 타입스크립트 빌드 시간을 최적화하는 데 도움이 되는 몇 가지 방법들이 있습니다.
tsconfig.json 설정skipLibCheck: true: 선언 파일 자체의 검사를 건너뜁니다.node_modules에만 한정되지 않으며, 중복되거나 충돌하는 선언의 오류를 가릴 수 있으므로 검사 비용과 정확성의 절충을 판단합니다. 애플리케이션 코드가 그 타입을 사용하는 검사는 계속됩니다.isolatedModules: true: Babel이나 esbuild처럼 파일을 하나씩 변환하는 도구가 올바르게 해석할 수 없는 구문을 TypeScript가 진단하도록 합니다. 모든 파일을 모듈로 바꾸거나 변환을 빠르게 만드는 옵션은 아닙니다. 타입 검사와 실제 변환을 나누는 파이프라인의 제약을 확인할 때 사용합니다.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에서 같은 기준으로 검증되어야 합니다.
아래는 최적화를 평가하는 측정 절차이며, 이 절에서 수행한 벤치마크 결과를 뜻하지 않습니다.
MEASURE → CHANGE → VERIFY
측정한 병목에 한 번의 변경을 적용하고 다시 검증한다
type-check·transpile·bundle·test를 분리해 cold와 warm, 로컬과 CI를 같은 조건으로 측정합니다. 병목이 재현되면 관련된 한 요소를 바꾸고 시간과 결과 정확성을 함께 비교합니다.
BASELINE
캐시 조건을 명시한다
cold는 재사용 상태 없이, warm은 같은 조건의 재사용 상태로 비교합니다. tool/version·입력·옵션·machine·CI 조건과 cache hit를 함께 기록합니다.
ROUTE
재현되지 않으면 측정을 점검
한 번 빠르거나 느린 결과만으로 최적화를 선택하지 않습니다. 환경·입력·캐시 차이를 먼저 설명하고 같은 병목이 반복되는지 확인합니다.
CHANGE
병목의 책임에 맞춘다
검사 범위, project references, 증분 재사용, 변환 도구를 해당 단계에 맞춰 검토합니다. transpiler를 바꿔도 별도의 TypeScript 타입 검사는 필요합니다.
VERIFY
정확성과 속도를 같이 비교
같은 지원 구성에서 변경 전후의 진단·JS·선언·테스트 결과와 시간을 비교합니다. skipLibCheck처럼 오류를 숨길 수 있는 절충도 기록합니다.
이 그림은 측정 절차입니다. 교재에서 수행한 벤치마크 수치가 아닙니다.
연습: 캐시가 있어도 오류와 산출물은 따로 확인합니다
다음은 TypeScript 6.0.3에서 확인할 수 있는 단일 프로젝트 예제입니다. 일반 tsc -p tsconfig.json을 사용하며, 프로젝트 레퍼런스나 watch 프로세스는 사용하지 않습니다. src/index.ts 외에 소스는 없고, 다른 도구가 출력 폴더를 지우거나 파일을 생성하지 않는다고 가정합니다.
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
"types": [],
"incremental": true,
"tsBuildInfoFile": "./.cache/build.tsbuildinfo",
"rootDir": "./src",
"outDir": "./dist",
"noEmitOnError": true
},
"include": ["src/**/*.ts"]
}export const total: number = 3;- 처음 성공한 뒤
.cache/build.tsbuildinfo만 없어지고dist/index.js는 남았습니다. 이 캐시는 앱 실행에 필요한 import 대상일까요? 다음 빌드는 캐시가 없다는 이유만으로 실패해야 할까요? - 첫 빌드 성공 뒤 캐시와 JS를 유지한 상태에서 소스를
export const total: number = "3";으로 바꿔 다시 검사합니다. 캐시가 오류를 통과시킬까요? 기존 JS가 남아 있다는 사실은 새 소스가 검사에 통과했다는 뜻일까요? - 두 번째 상황에서
skipLibCheck: true만 추가하면 애플리케이션의 이 타입 오류가 사라질까요? 오류를 고친 뒤noEmit: true로 바꾸면, 성공한 타입 검사가 새로운 JS 생성을 보장할까요?
캐시·타입 검사·이전 JS의 풀이
.tsbuildinfo는 컴파일러용 캐시이며 실행할 JS가 읽는 파일이 아닙니다. 캐시만 없어져도 기존 JS는 그 이유만으로 실행 불가능해지지 않습니다. 다음 빌드는 소스에서 정보를 다시 만들 수 있습니다. 다만 캐시를 읽는 warm 빌드와 같다고 측정하면 안 됩니다. 이 설명은 출력 파일까지 지워졌다는 가정이 아닙니다.- 문자열은
number에 대입할 수 없으므로 타입 오류가 납니다. 변경된 입력에 대해 캐시를 재사용할 수 있는지를 판단하므로 캐시는 오류 검사 면제권이 아닙니다.noEmitOnError: true는 이 오류가 있는 실행에서 새 JS 출력을 막지만, 앞 성공에서 만들어진dist/index.js를 자동 삭제하지 않습니다. 따라서 남아 있는 JS와 최근 검사 성공 여부를 따로 기록해야 합니다. 빌드 정보 파일 갱신 여부도 JS 출력과 구분합니다. skipLibCheck는 선언 파일 자체의 검사를 생략하는 옵션이고,src/index.ts의 문자열→숫자 대입을 허용하지 않습니다. 오류를 고치면 검사는 성공할 수 있지만,noEmit: true는 JS 생성을 하지 않도록 정한 별도 조건입니다. 번들러가 JS를 만들도록 구성했다면 그 단계의 성공과 검사 성공도 별도로 확인해야 합니다.
시간을 비교할 때 함께 적을 결과
예를 들어 cold 1회와 warm 1회의 시간이 다르더라도, 입력·컴파일러 버전·옵션·실행 환경과 캐시 조건이 같았는지부터 확인합니다. 타입 오류를 가리는 옵션을 추가한 뒤 빠른 수치만 비교하면 검사 범위가 줄어든 것을 동일한 최적화로 오해할 수 있습니다. 이 연습은 진단과 출력의 관계를 확인하며, 실제 앱의 빌드 시간이나 번들러 성능을 측정한 벤치마크는 아닙니다.