빌드·번들 툴체인, 무엇부터 봐야 할까?

빌드와 번들 성능을 가르는 세 가지 축이 있다. 증분성(incrementality) — 변경된 부분만 다시 컴파일하는가. 캐시 무효화(cache invalidation) — 언제 캐시를 버릴지 판단하는 로직이 보수적인가, 적극적인가. 병렬성(parallelism) — 워커 스레드가 얼마나 효율적으로 의존성을 타고 퍼져나가는가. 이 세 축의 조합이 콜드 빌드 30초, 웜 빌드 1초의 차이를 만든다.

증분 빌드: 정말 변경된 것만 다시 컴파일되는가?

증분 빌드는 파일 변경을 감지하고 그 영향 범위를 정확히 그려내는 두 단계로 나뉜다. 첫 단계는 파일 시스템 이벤트 감지다. 대부분의 도구는 수정 시간(mtime)과 파일 해시를 기반으로 변경을 판단한다. 문제는 빌드 과정에서 자동 생성되는 임시 파일이 mtime을 갱신하면, 영향받지 않은 모듈까지 무효화될 수 있다는 점이다.

두 번째 단계는 의존성 그래프 추적이다. A 모듈이 B 모듈을 import하고, B가 C를 import할 때, A를 수정하면 B와 C는 다시 컴파일할 필요가 없다. 그러나 B의 public API 구조가 바뀌면(함수 시그니처, 타입 정의 등) A도 재컴파일해야 한다. 이를 public 인터페이스 해시(public interface hash) 추적이라 하는데, 전체 파일 내용이 아닌 노출된 심볼만 해시하면 불필요한 재컴파일을 줄일 수 있다.

JavaScript/TypeScript 생태계의 esbuild나 swc는 이 추적을 파일 단위로 한다. Python의 pyproject.toml이나 Java의 gradle은 모듈(패키지) 단위 그래프를 유지한다. Rust의 cargo는 크레이트 수준의 공개 API 변경을 감지하려 시도하지만, 프로시저 매크로나 빌드 스크립트가 개입하면 정확성이 떨어진다.

캐시 무효화 정책: 보수적 vs. 적극적?

캐시를 언제 버릴지는 보수성(false negative 없음) vs. 효율(false positive 최소) 사이의 선택이다.

보수적 정책: 의존성 추적이 불완전할 수 있으니, 상위 모듈이 변경되면 하위 모듈도 모두 무효화한다. 예를 들어 package.json의 버전 문자열만 바뀌어도 모든 의존 파일을 재빌드한다. 정확하지만 느리다.

적극적 정책: 의존성 그래프를 세밀하게 추적해서 실제로 영향받을 대상만 무효화한다. 공개 API가 바뀌지 않으면 파일 내용 변경은 무시한다. 이를 위해서는 content hashinterface hash를 분리해서 유지해야 한다.

2026년 기준, 대부분의 모던 도구는 적극적 정책으로 옮겨가고 있다. Turbopack, Vite의 HMR, gradle의 configuration cache는 모두 인터페이스 단위 추적을 시도한다. 다만 구현 복잡도가 높아서, 정책을 잘못 설정하면 캐시가 오염되어 더 느려질 수 있다.

병렬 빌드와 워크스페이스: 의존성 그래프가 얼마나 넓은가?

병렬성의 상한선은 의존성 그래프의 **임계 경로(critical path)**가 정한다. A → B → C 직렬 의존성은 아무리 CPU 코어가 많아도 3 단계 시간보다 빨라질 수 없다. 반면 A → {B, C, D} 병렬 의존성은 4 코어면 2 단계에 끝낼 수 있다.

워크스페이스 기반 빌드 시스템(npm workspaces, cargo workspace, gradle multi-project)은 이 병렬성을 최대화하려 설계됐다. 각 패키지/크레이트가 명시적 의존성을 선언하면, 빌드 시스템이 DAG(방향성 비순환 그래프)를 구성하고 독립적인 작업을 동시에 실행한다.

실측 예시를 보면:

  • 동기식 직렬: 100개 패키지, 각 1초 → 총 100초
  • 워크스페이스 4 병렬: 같은 구조, 임계 경로 20초 → 총 ~25초 (오버헤드 포함)

이 효과는 모듈 수가 늘수록 커진다. 50개 모듈 이상 모노레포에서는 병렬 빌드 없이 웜 빌드만 30초를 넘긴다.

콜드 vs. 웜 빌드: 캐시 가용성이 반영되는 방식?

콜드 빌드: 캐시가 없거나 완전히 무효화된 상태. 첫 번째 빌드, 또는 npm install 후 첫 빌드. 전체 소스를 파싱하고 컴파일해야 한다.

웜 빌드: 이전 빌드 캐시가 유효한 상태. 변경 파일만 처리한다.

일반적인 개발 루프에서는 웜 빌드가 대부분이다. 따라서 도구 선택은 웜 빌드 시간에 더 민감해야 한다.

시나리오 영향 요소
콜드 빌드 전체 컴파일 능력, 직렬화 오버헤드
웜 빌드 (single file change) 의존성 그래프 정확성, 캐시 조회 속도
웜 빌드 (package.json 수정) 캐시 무효화 정책의 보수성
깨진 캐시 복구 캐시 무효화 감지 속도

Rust 기반 도구(swc, esbuild 대체제들)는 보통 콜드 빌드에서 5~15% 빨라지지만, 웜 빌드에서는 캐시 조회 오버헤드가 상대적으로 커져서 큰 이득이 없을 수 있다. 주의해야 할 점은, 언어 선택이 아니라 캐시 구조 설계가 실제 성능을 결정한다는 것이다.

모노레포 확장과 증분성: 일부 패키지 수정이 전체를 무효화하지 않는가?

모노레포는 여러 패키지(또는 크레이트)를 하나의 리포지토리에서 관리한다. 이 구조에서는 패키지 간 의존성 추적이 핵심이다.

계층적 의존성: A → B → C 구조에서 C를 수정해도 A는 다시 빌드할 필요 없다. 단, B의 공개 인터페이스가 변경되면 A도 함께 무효화된다.

루트 설정 파일 변경: package.json, workspace 루트의 tsconfig.json을 수정하면, 보수적인 도구는 모든 패키지를 무효화한다. 적극적인 도구는 실제 영향받는 패키지만 선별한다.

링크 의존성: 모노레포 내 패키지가 심볼릭 링크나 상대 경로로 참조되면, 버전 번호 무효화가 작동하지 않는다. 파일 해시에만 의존하게 되는데, 이는 거짓 양성(false positive)을 높인다.

규모가 커질수록(100+ 패키지) 이 문제가 드러난다. 도구 선택 시 패키지 격리 수준을 확인해야 한다. 일부 도구는 의존성 격리를 위해 추가 메타데이터(manifest hash)를 유지한다.

Rust 기반 도구 전환: 측정 가능한 이득이 있는가?

2026년 기준, JavaScript 생태계에서 Rust 기반 도구(swc, esbuild 계열 C++ 바인딩 대체제, turbopack 등)로의 전환이 진행 중이다. 이득을 실측하려면 파싱 속도, 코드 생성 속도, 메모리 효율을 분리해서 봐야 한다.

파싱: Rust 파서가 JavaScript 파서보다 23배 빠를 수 있다. 단, 현대 JavaScript 문법(destructuring, async/await, JSX)을 완전히 지원하려면 복잡도가 올라간다. 실측 결과는 문법 부하에 따라 1030% 범위에서 변한다.

코드 생성: 최적화 수준이 같으면 언어 이점이 작다. Rust 도구도 결국 같은 알고리즘을 따른다. 다만 메모리 할당 패턴이 다르면 GC 일시 정지 문제를 피할 수 있다.

메모리: Rust 도구는 보통 메모리 소비가 적다. Node.js 프로세스(V8 엔진)는 초기 힙이 3050MB인데, Rust 도구는 1015MB 수준이다. 이는 대규모 모노레포(5000+ 파일)에서 의미 있다.

중요한 맥락은, 언어 선택 자체보다 아키텍처 개선이 더 큰 이득을 낸다는 것이다. Turbopack이 웹팩보다 빠른 이유는 Rust 때문이 아니라, 증분성과 병렬성을 처음부터 설계한 것 때문이다.

용도별 도구 선택: 무엇이 선택을 나누는가?

소규모 SPA/정적 사이트 (파일 <500):

  • 콜드 빌드가 자주 일어난다.
  • 캐시 오버헤드를 감수할 가치가 적다.
  • esbuild, swc 같은 최소 도구가 충분하다.

중규모 프레임워크 기반 앱 (패키지 20~100):

  • 웜 빌드가 대부분이다.
  • 워크스페이스 수준 병렬성이 필요하다.
  • Turbo, nx, gradle 같은 작업 스케줄러가 가치 있다.

대규모 모노레포 (패키지 100+, 파일 10000+):

  • 캐시 무효화 정책이 성능을 좌우한다.
  • 분산 캐시(Turbo Remote Cache, Gradle Build Cache)가 필수다.
  • 의존성 격리와 public API 추적이 핵심이다.

흔한 실수: 캐시 설정이 성능을 악화시키는 경우?

많은 팀이 캐시를 무조건 활성화하면 빨라진다고 가정한다. 실제로는 캐시 조회 오버헤드, 무효화 오탐, 직렬화 비용이 순이득을 음수로 만들 수 있다.

무효화 오탐의 예:

  • 의존성 추적 로직이 파일 경로 정규화를 제대로 하지 않음 → 같은 파일을 다른 경로로 참조 → 캐시 미스
  • 빌드 시간(timestamp)을 입력으로 포함 → 매 빌드마다 캐시 무효화
  • 환경 변수를 캐시 키로 포함하지 않음 → 환경 전환 시 stale 캐시 사용

조회 오버헤드의 예:

  • 대용량 캐시 인덱스(수백만 엔트리)를 메모리에 올리면 시작 시간이 증가
  • 캐시 저장소가 원격(AWS S3, Redis)이면 네트워크 지연 누적

실제 개선은 캐시 정책을 프로파일링으로 검증한 후, 필요한 부분만 선택적으로 활성화하는 것이다. "모든 빌드 캐시"보다 "리소스 집약적 단계(예: 타입스크립트 컴파일, 이미지 최적화)만 캐시"가 더 효과적일 수 있다.

핵심 정리

  • 증분 빌드의 정확성: 파일 해시와 의존성 그래프 추적의 조합으로, public API 변경만 감지하면 불필요한 재컴파일을 줄일 수 있다.

  • 캐시 무효화 정책: 보수적 정책은 안전하지만 느리고, 적극적 정책은 빠르지만 복잡하다. 팀의 의존성 규율 수준에 맞춰 선택해야 한다.

  • 병렬성의 상한: 의존성 DAG의 임계 경로가 병렬 빌드 이득의 상한을 정한다. 패키지 수가 50개 이상이면 병렬 스케줄러의 가치가 명확하다.

  • 웜 빌드가 핵심: 일상적인 개발 루프에서는 웜 빌드 시간이 경험을 좌우한다. 도구 평가 시 변경된 파일 기준의 성능을 우선해야 한다.

  • Rust 기반 도구의 실질: 언어 선택 자체보다 아키텍처(증분성, 병렬성 설계)가 성능을 결정한다. 파싱 속도 1030% 개선은 전체 빌드에서 15% 정도만 기여한다.

  • 모노레포는 격리가 열쇠: 패키지 간 의존성을 정확히 추적하지 않으면, 작은 변경이 전체를 무효화한다. 100+ 패키지 규모에서는 필수.

  • 캐시 정책 검증: 캐시를 무조건 활성화하는 것보다, 무효화 오탐과 조회 오버헤드를 프로파일링으로 측정한 후 선택적으로 도입하는 것이 효율적이다.

자주 묻는 질문

증분 빌드를 적용하면 정말 몇 배 빨라지나?

변경 범위에 따라 다르다. 단일 파일 수정, 공개 인터페이스 무변경: 510배. 단일 파일 수정, 인터페이스 변경: 23배. 의존성 추적이 정확하지 않으면 효과가 떨어지거나 무효화된다.

모노레포에서 캐시를 활성화하면 속도가 더 느려질 수도 있나?

네. 캐시 조회 오버헤드, 무효화 오탐, 직렬화 비용이 캐시 히트로 얻는 이득보다 크면 역효과가 난다. 특히 캐시 인덱스가 수백만 엔트리를 넘으면, 시작 시간 자체가 증가한다.

Rust 도구로 전환하면 전체 빌드 시간이 얼마나 단축되나?

언어 전환 자체는 5~15% 정도다. 더 큰 이득을 원하면 아키텍처를 함께 개선해야 한다(증분성, 병렬성, 캐시 정책).

의존성 그래프가 복잡하면 병렬 빌드가 효과 없을까?

아니다. 복잡할수록 병렬화할 부분이 많다. 다만 임계 경로가 길면(주요 의존 체인이 깊으면) 아무리 병렬화해도 그 경로 시간은 단축되지 않는다.

언제부터 분산 캐시(원격 캐시)를 도입해야 하나?

팀 규모 3명 이상, 패키지 50개 이상, 콜드 빌드 시간 30초 이상일 때. 그 이전에는 로컬 캐시만으로도 충분하다.

타입스크립트 프로젝트에서 캐시 무효화가 자주 일어나는 이유는?

타입스크립트 컴파일러는 상대 경로, 경로 별칭(path alias), tsconfig.json 설정을 의존성에 포함한다. 이들 중 하나라도 변경되면 캐시가 무효화된다. 실제 소스 변경이 없어도.

esbuild가 충분할 때, Turbopack 같은 도구로 마이그레이션할 가치가 있나?

esbuild는 번들 속도에만 최적화되어 있고, 증분 빌드와 워크스페이스 병렬화를 지원하지 않는다. 모노레포 규모가 30패키지 이상이면, 이들 기능이 필요해진다.