Next.js Turbopack 빌드 캐시로 `next build`를 10배 빠르게 — 실험적 플래그의 함정과 CI 연동 전략
CI에서 next build가 3분을 넘어가기 시작하면 슬슬 스트레스가 쌓입니다. PR 하나 올릴 때마다 배포 파이프라인 앞에서 커피 한 잔을 다 마셔야 하는 그 시간. 저도 e-커머스 프로젝트에서 webpack 빌드가 180초를 넘기면서 "이걸 어떻게 줄여보나" 하고 한참 고민했습니다. 로컬에서 스테이징 배포 전 빌드 확인할 때마다 그 대기 시간이 답답했고, CI에서도 마찬가지였죠. Turbopack의 파일시스템 캐시는 정확히 그 문제를 겨냥한 기능입니다.
이 글을 다 읽고 나면, CI 빌드 3분을 30초 안으로 줄이는 설정을 바로 적용할 수 있습니다. 핵심은 단순합니다 — 변경된 파일만 다시 컴파일하고, 나머지는 디스크에서 그대로 꺼내 씁니다. 단, 빌드용 캐시는 아직 실험적 단계라 함정이 있습니다. 속도 개선만큼 중요한 주의사항도 함께 정리했습니다.
핵심 개념
증분 빌드란 무엇인가
기존 webpack 빌드는 매번 전체 의존성 그래프(모든 파일 사이의 import 관계를 추적한 구조)를 처음부터 다시 계산합니다. 파일 하나 고쳤는데 앱 전체를 다시 번들링하는 셈이죠. Turbopack의 FileSystem Cache는 이 방식을 뒤집습니다.
빌드 결과물(의존성 그래프, 중간 AST(소스 코드를 트리 형태로 파싱한 구조), 변환된 모듈 등)을 .next 디렉터리에 영속적으로 저장해두고, 다음 빌드 때는 변경된 파일에 영향받는 부분만 재연산합니다. 이게 증분(incremental) 빌드 캐싱입니다.
현재 두 가지 플래그로 나뉘어 있습니다:
| 플래그 | 대상 | 상태 |
|---|---|---|
turbopackFileSystemCacheForDev |
next dev |
Next.js 16.1 릴리즈 노트 기준 안정화·기본 활성화 |
turbopackFileSystemCacheForBuild |
next build |
현재 opt-in 실험적 플래그 |
활성화 방법은 간단합니다:
// next.config.ts
const nextConfig = {
experimental: {
turbopackFileSystemCacheForBuild: true,
},
};
export default nextConfig;TurboEngine — 셀 단위 캐싱의 원리
내부를 조금 파보면 흥미로운 구조가 있습니다. Turbopack 내부에는 TurboEngine이라는 Rust 기반 증분 메모이제이션 프레임워크가 동작하고 있습니다. 처음 이 구조를 접했을 때 "그냥 파일 해시 비교 아닌가?"라고 생각했는데, 실제로는 훨씬 정교합니다.
TurboEngine은 함수 호출 결과를 "값 셀(value cell)" 그래프로 관리합니다. 소스 파일이 변경되면 영향받는 셀만 dirty 처리하고, 셀 내용이 이전과 동일하면 상위 그래프로의 전파를 조기 차단(short-circuit)합니다.
예를 들어 utils/format.ts 파일 하나를 수정했다고 가정하면, TurboEngine은 이 파일을 참조하는 모듈 체인만 추적해서 재연산합니다. 그 체인의 출력이 이전과 동일하면, 그보다 상위에 있는 모듈들은 재빌드를 건너뜁니다. 파일 단위가 아닌 함수 결과 셀 단위의 추적이라 훨씬 세밀한 캐시 무효화가 가능합니다.
Rust 기반이라는 점도 속도에 직접적인 영향을 줍니다. Rust는 GC(가비지 컬렉션) 없이 메모리를 관리하기 때문에, 대규모 의존성 그래프를 처리할 때 JavaScript 기반 도구 대비 오버헤드가 크게 줄어듭니다.
webpack 5 Persistent Cache와의 차이
webpack을 쓰다가 Turbopack으로 넘어온 경우라면 cache: { type: 'filesystem' } 옵션이 낯설지 않으실 겁니다. 개념 자체는 비슷하지만, 차이가 있습니다:
| 비교 항목 | webpack 5 Persistent Cache | Turbopack FileSystem Cache |
|---|---|---|
| 추적 단위 | 파일·청크(여러 모듈을 묶은 출력 단위) 단위 | 함수 결과 셀 단위 |
| 구현 언어 | JavaScript | Rust |
| 무효화 정확도 | 보수적 (과도한 재빌드 발생 가능) | 세밀한 의존성 추적 |
| 캐시 무효화 제어 | 개발자가 buildDependencies 등 수동 설정 |
자동 추적, 수동 설정 불필요 |
실전 적용
예시 1: 로컬 반복 빌드 최적화
가장 체감이 큰 시나리오입니다. 스테이징 배포 전 로컬에서 빌드를 자주 확인하는 경우, next.config.ts에 플래그 하나만 추가하면 됩니다.
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
turbopackFileSystemCacheForBuild: true,
},
}
export default nextConfig활성화 후 빌드를 두 번 돌려보시면 차이를 바로 느끼실 수 있습니다:
# 첫 번째 빌드 (cold) — 캐시 없음
$ pnpm build
# ✓ Compiled successfully in 15.2s
# 파일 일부 수정 후 두 번째 빌드 (warm) — 캐시 활용
$ pnpm build
# ✓ Compiled successfully in 1.1s아래는 Vercel이 자사 앱들을 1년 이상 dogfooding하며 측정한 공식 벤치마크입니다. 대규모 앱일수록 개선 배율이 커지는 경향이 있습니다:
| 앱 | Cold 빌드 | Cached 빌드 | 개선 배율 |
|---|---|---|---|
| react.dev | 3.7s | 380ms | ~10× |
| nextjs.org | 3.5s | 700ms | ~5× |
| 대형 내부 앱 | 15s | 1.1s | ~14× |
프로젝트 규모에 따라 수치는 다를 수 있습니다. 중간 규모 앱에서는 5× 전후가 현실적인 기대치입니다.
예시 2: GitHub Actions CI/CD 연동
로컬에서만 캐시가 살아있으면 의미가 반감됩니다. CI에서도 .next 디렉터리를 캐싱하면 파이프라인 전체 속도를 높일 수 있습니다.
# .github/workflows/build.yml
name: Build
on:
push:
branches: [main, develop]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9 # 프로젝트 버전에 맞게 수정
- uses: actions/setup-node@v4
with:
node-version: 22 # 프로젝트 버전에 맞게 수정
cache: 'pnpm'
- name: Cache .next directory
uses: actions/cache@v4
with:
path: .next
key: ${{ runner.os }}-next-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.ts', 'src/**/*.tsx') }}
restore-keys: |
${{ runner.os }}-next-${{ hashFiles('**/pnpm-lock.yaml') }}-
${{ runner.os }}-next-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build캐시 키 설계에서 한 가지 포인트가 있습니다. pnpm-lock.yaml이 바뀌면(의존성 변경) 캐시를 버리고, src/ 안의 TypeScript 파일이 바뀌면 restore-keys를 통해 부분 히트를 시도합니다. 완벽한 캐시 미스보다 부분 히트가 훨씬 낫기 때문입니다. **/*.ts처럼 범위를 너무 넓게 잡으면 node_modules 안의 타입 파일까지 해시 대상이 되어 불필요하게 캐시 키가 바뀔 수 있으니, src/**/*.ts로 좁히는 편이 현실적입니다.
플래그 활성화는 next.config.ts에서 관리하는 것이 기본입니다. CI 환경 변수로 제어하는 방법은 공식 문서에서 별도로 안내하지 않으므로, 설정 파일 기준으로 운용하는 것을 권장합니다.
예시 3: 캐시 오염 발생 시 초기화
실무에서 "빌드는 성공했는데 런타임에서 이상하게 동작한다"거나 "빌드 자체가 알 수 없는 에러로 실패한다"는 상황이 생길 수 있습니다. 캐시 부패(corruption)를 의심해볼 시점입니다.
# 캐시 전체 초기화 — 이 순서대로 삭제하는 것을 권장합니다
rm -rf .next
rm -rf node_modules/.cache
rm -rf .turbo
# 클린 빌드
pnpm build캐시를 지우는 게 아깝게 느껴질 수 있지만, 이 상황에서는 가장 확실한 방법입니다. 이후 빌드는 cold 빌드가 되지만, 이상 증상이 캐시 문제였다면 바로 해결됩니다.
장단점 분석
솔직히 장점 목록만 보면 당장 프로덕션에 올리고 싶어지는데, 제가 실제로 겪어보니 단점 쪽이 더 뼈아팠습니다. 둘 다 냉정하게 정리해봤습니다.
장점
| 항목 | 내용 |
|---|---|
| 반복 빌드 속도 | warm 빌드 기준 수십 초 → 수백 밀리초. 실체감이 있는 수준의 개선 |
| 세밀한 캐시 무효화 | 파일 단위가 아닌 셀 단위 추적으로 과도한 재빌드 방지 |
| 자동 추적 | 개발자가 캐시 키나 의존성을 수동 관리할 필요 없음 |
next dev 재시작 개선 |
이전 컴파일 상태에서 즉시 재개, 워밍업 시간 없음 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 빌드용은 실험적 상태 | 캐시 무효화 정확성 검증이 진행 중 | 프로덕션 배포 전 충분한 테스트 권장 |
| 캐시 부패 위험 | 캐시 오염으로 인한 빌드 오류 사례가 Vercel 커뮤니티에 보고됨 | .next, node_modules/.cache, .turbo 삭제 후 재빌드 |
| CI 오버헤드 | .next 캐시가 커질수록 업로드·다운로드 시간 증가. 캐시 히트율(전체 빌드 중 재연산을 건너뛴 비율)이 80% 미만이면 효과 감소 |
캐시 키 전략을 세밀하게 조정 |
| 팬텀 모듈 참조 버그 | 삭제된 파일을 캐시가 여전히 참조하는 케이스가 간헐적으로 존재 (GitHub Discussion #89669) | 캐시 전체 초기화로 해결 가능 |
| 환경 변수 무효화 | 빌드 환경 변수 변경 시 캐시가 제대로 무효화되는지 확인 필요 | 환경 변수 변경 후 첫 배포는 클린 빌드 권장 |
실무에서 가장 흔한 실수
- webpack cold vs Turbopack warm을 비교하는 것. 동일 조건(둘 다 cold 또는 둘 다 warm)으로 비교해야 정확한 베이스라인 차이를 알 수 있습니다. 캐시 없는 첫 빌드끼리 비교해보시면 실제 차이를 확인할 수 있습니다.
- CI 캐시 키를 너무 넓게 잡는 것. 의존성 변경 시 캐시를 버릴 수 있도록 키에 락파일 해시를 포함시키는 것을 권장합니다. 그렇지 않으면 오염된 캐시가 계속 복원될 수 있습니다.
- 캐시 부패 증상을 코드 버그로 오인하는 것. 빌드는 성공했는데 런타임 동작이 이상하거나, 분명히 수정한 내용이 반영이 안 되는 것처럼 보일 때 캐시 초기화를 먼저 시도해보시면 좋습니다.
마치며
Turbopack FileSystem Cache는 반복 빌드 속도를 수 배~수십 배 개선하는 실질적인 기술이지만, 빌드용 캐시는 아직 실험적 단계인 만큼 캐시 무효화 정확성을 충분히 검증하면서 도입하는 것이 현명합니다.
PR 올릴 때마다 마주하던 그 3분 빌드 대기 시간, 이제 달라질 수 있습니다. 지금 바로 시작해볼 수 있는 3단계입니다:
- 플래그 추가 후 비교 측정 —
next.config.ts에experimental: { turbopackFileSystemCacheForBuild: true }를 추가하고,pnpm build를 두 번 실행해 cold/warm 빌드 시간 차이를 직접 측정해보시면 좋습니다. - CI 캐시 연동 —
actions/cache@v4로.next디렉터리를 캐싱하고, 캐시 키에pnpm-lock.yaml해시를 포함시켜 의존성 변경 시 자동으로 갱신되도록 설정할 수 있습니다. - 배포 전 클린 빌드 검증 —
rm -rf .next && pnpm build결과를 캐시 빌드와 비교해 출력이 일치하는지 확인하는 습관을 들이시면, 캐시 부패로 인한 예상치 못한 이슈를 사전에 방지할 수 있습니다.
참고 자료
- next.config.js: turbopackFileSystemCache | Next.js 공식 문서 (본문 인용)
- Next.js 16 공식 릴리즈 노트
- Next.js 16.1 공식 릴리즈 노트 (본문 인용)
- Inside Turbopack: Building Faster by Building Less (본문 인용)
- Turbopack File System Caching Feedback · vercel/next.js Discussion #87283
- Turbopack Phantom Module References Causing Panic · Discussion #89669 (본문 인용)
- Turbopack Persistent Caching — Tobias Koppers (JSNation)
- Next.js 16.2 Turbopack: The Anatomy of a 400% Improvement — DEV Community
- Turbopack corrupted build cache — Vercel Community (본문 인용)
- next.config.js: turbopackPersistentCaching | Next.js 15 문서