개인정보처리방침© 2026 DEV BAK - 기술블로그. All rights reserved.
DEV BAK - 기술블로그
frontend

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 실험적 플래그

활성화 방법은 간단합니다:

typescript
// 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에 플래그 하나만 추가하면 됩니다.

typescript
// next.config.ts
import type { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  experimental: {
    turbopackFileSystemCacheForBuild: true,
  },
}
 
export default nextConfig

활성화 후 빌드를 두 번 돌려보시면 차이를 바로 느끼실 수 있습니다:

bash
# 첫 번째 빌드 (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 디렉터리를 캐싱하면 파이프라인 전체 속도를 높일 수 있습니다.

yaml
# .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)를 의심해볼 시점입니다.

bash
# 캐시 전체 초기화 — 이 순서대로 삭제하는 것을 권장합니다
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) 캐시 전체 초기화로 해결 가능
환경 변수 무효화 빌드 환경 변수 변경 시 캐시가 제대로 무효화되는지 확인 필요 환경 변수 변경 후 첫 배포는 클린 빌드 권장

실무에서 가장 흔한 실수

  1. webpack cold vs Turbopack warm을 비교하는 것. 동일 조건(둘 다 cold 또는 둘 다 warm)으로 비교해야 정확한 베이스라인 차이를 알 수 있습니다. 캐시 없는 첫 빌드끼리 비교해보시면 실제 차이를 확인할 수 있습니다.
  2. CI 캐시 키를 너무 넓게 잡는 것. 의존성 변경 시 캐시를 버릴 수 있도록 키에 락파일 해시를 포함시키는 것을 권장합니다. 그렇지 않으면 오염된 캐시가 계속 복원될 수 있습니다.
  3. 캐시 부패 증상을 코드 버그로 오인하는 것. 빌드는 성공했는데 런타임 동작이 이상하거나, 분명히 수정한 내용이 반영이 안 되는 것처럼 보일 때 캐시 초기화를 먼저 시도해보시면 좋습니다.

마치며

Turbopack FileSystem Cache는 반복 빌드 속도를 수 배~수십 배 개선하는 실질적인 기술이지만, 빌드용 캐시는 아직 실험적 단계인 만큼 캐시 무효화 정확성을 충분히 검증하면서 도입하는 것이 현명합니다.

PR 올릴 때마다 마주하던 그 3분 빌드 대기 시간, 이제 달라질 수 있습니다. 지금 바로 시작해볼 수 있는 3단계입니다:

  1. 플래그 추가 후 비교 측정 — next.config.ts에 experimental: { turbopackFileSystemCacheForBuild: true }를 추가하고, pnpm build를 두 번 실행해 cold/warm 빌드 시간 차이를 직접 측정해보시면 좋습니다.
  2. CI 캐시 연동 — actions/cache@v4로 .next 디렉터리를 캐싱하고, 캐시 키에 pnpm-lock.yaml 해시를 포함시켜 의존성 변경 시 자동으로 갱신되도록 설정할 수 있습니다.
  3. 배포 전 클린 빌드 검증 — 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 문서
#NextJS#Turbopack#증분빌드#빌드캐시#TypeScript#GitHubActions#CI-CD#webpack#Rust#pnpm
공유하기

목차

핵심 개념증분 빌드란 무엇인가TurboEngine — 셀 단위 캐싱의 원리webpack 5 Persistent Cache와의 차이실전 적용예시 1: 로컬 반복 빌드 최적화예시 2: GitHub Actions CI/CD 연동예시 3: 캐시 오염 발생 시 초기화장단점 분석장점단점 및 주의사항실무에서 가장 흔한 실수마치며참고 자료

추천 포스트

Next.js RSC + Streaming으로 TTFB를 350ms에서 60ms로 단축하는 방법
frontend

Next.js RSC + Streaming으로 TTFB를 350ms에서 60ms로 단축하는 방법

운영 중인 서비스의 Core Web Vitals를 들여다보다가 TTFB가 400ms를 훌쩍 넘는 페이지를 발견한 적이 있습니다. DB 쿼리는 각각 빠른데 왜 이렇게 느리지 싶어서 살펴봤더니, 세 개의 독립적인 쿼리가 직렬로 대기하고 있었습니다. 로 병렬화하면 해결되겠구나 싶었는데, 막상...

2026년 06월 07일읽는 데 19분
HTMX 4.0 서버 렌더링 패턴: 클라이언트 상태 없이 인터랙티브 웹을 만드는 아키텍처 선택
frontend

HTMX 4.0 서버 렌더링 패턴: 클라이언트 상태 없이 인터랙티브 웹을 만드는 아키텍처 선택

React를 처음 배울 때, 저는 솔직히 이 질문을 머릿속에 묻어뒀습니다. "버튼 클릭 하나에 이렇게 많은 코드가 필요한 게 맞는 건가?" 상태 관리, 빌드 도구, hydration 오류... 분명 웹은 HTML과 폼으로 동작하던 시절이 있었는데, 어느 순간 그 단순함을 잃어버렸다는 느...

2026년 06월 07일읽는 데 22분
View Transitions API — 2025 Baseline 달성 이후, 라이브러리 없이 구현하는 프로덕션 전환 애니메이션
frontend

View Transitions API — 2025 Baseline 달성 이후, 라이브러리 없이 구현하는 프로덕션 전환 애니메이션

솔직히 처음 이 API를 봤을 때 "이게 된다고?" 싶었습니다. Framer Motion이나 GSAP 없이, CSS 몇 줄과 JavaScript 메서드 하나만으로 앱 같은 화면 전환을 구현한다는 게 너무 좋아 보였거든요. 그런데 실제로 써보니 정말 됩니다. 브라우저가 화면 전환 전후 스...

2026년 06월 07일읽는 데 20분
Zustand persist migrate: persistedState unknown 타입을 TypeScript로 안전하게 좁히는 4가지 방법
frontend

Zustand persist migrate: persistedState unknown 타입을 TypeScript로 안전하게 좁히는 4가지 방법

Zustand로 상태를 localStorage에 영속화하다 보면, 어느 순간 스키마를 바꿔야 하는 상황이 옵니다. 필드 이름을 바꾸거나, 새 필드를 추가하거나, 중첩 구조를 평탄화하거나. 그럴 때 의 옵션을 꺼내 드는데, 처음 마주치면 꽤 당혹스러운 타입이 하나 있습니다. 바로 . ...

2026년 05월 30일읽는 데 19분
TanStack Query + Zustand: 서버 상태와 클라이언트 상태를 분리하는 패턴과 안티패턴
frontend

TanStack Query + Zustand: 서버 상태와 클라이언트 상태를 분리하는 패턴과 안티패턴

Redux를 쓰던 시절을 돌아보면, API 응답값과 모달 open/close 상태가 한 스토어 안에 뒤섞여 있었던 기억이 납니다. "이거 어디서 바뀐 거지?"라며 디버거를 붙잡고 씨름했던 날들이 꽤 많았죠. 그러다 TanStack Query(구 React Query)를 도입하면서 뭔가 ...

2026년 05월 24일읽는 데 19분
`@starting-style`로 팝오버(popover)·다이얼로그(dialog)에 순수 CSS 진입·퇴장 애니메이션 추가하기
frontend

`@starting-style`로 팝오버(popover)·다이얼로그(dialog)에 순수 CSS 진입·퇴장 애니메이션 추가하기

팝오버가 열릴 때마다 툭 튀어나오는 그 느낌, 한 번쯤은 거슬렸을 거예요. 닫힐 땐 또 그냥 사라지고. 부드럽게 만들어보려고 JS 코드를 열면 어김없이 이렇게 됩니다. 딱 이 패턴입니다. 클래스 토글하고, 이벤트 등록하고, 타이밍 맞추다가 엣지 케이스 터지고, 결국 타이머...

2026년 05월 22일읽는 데 17분