Turbopack이 Webpack보다 빠른 이유: 증분 컴파일, turbopack.rules 로더, 모노레포 튜닝, 마이그레이션
이 글의 핵심
Turbopack이 Webpack 대비 개발 서버·HMR에서 이점을 내는 구조적 이유와, Next.js 버전별 활성화 방법, next.config의 turbopack 옵션(rules·resolveAlias·root), 모노레포 최적화, 커스텀 Webpack 설정을 걷어내는 마이그레이션 절차를 실무 기준으로 연결합니다.
이 글의 핵심
Turbopack은 Vercel이 Webpack의 후속으로 개발한 Rust 기반 번들러로, Next.js에 내장되어 개발 서버의 시작 시간과 HMR을 개선하는 데서 출발해 Next.js 16부터는 개발과 프로덕션 빌드 모두의 기본 번들러가 되었습니다. 이 글은 Webpack과의 아키텍처 차이, 증분 컴파일이 빠른 조건과 느려지는 조건, next.config의 실제 설정 키, 모노레포에서의 루트와 해석 범위, Webpack 커스텀 설정을 걷어내는 절차를 설명합니다. Next.js App Router, next.config, 패키지 매니저 워크스페이스, Webpack의 module.rules를 알고 있다고 가정합니다.
Turbopack과 Webpack: 무엇이 달라 성능이 갈리나
구현 언어와 병렬성
Webpack은 Node.js 위에서 JavaScript로 동작하며, 방대한 플러그인 생태계가 강점입니다. 대규모 프로젝트에서는 영속 캐시나 thread-loader 같은 수단으로 속도를 보완하지만, 모듈 하나를 처리할 때마다 JavaScript로 작성된 로더와 플러그인 훅을 거치므로 단일 스레드 오버헤드가 누적됩니다.
Turbopack은 Rust로 작성되어 파싱과 변환(SWC)을 여러 스레드에서 병렬로 처리할 수 있습니다. 더 중요한 차이는 계산 모델입니다. Turbopack은 내부의 모든 작업을 입력과 출력이 기록되는 함수 호출 단위로 쪼개고 그 결과를 캐시해 두었다가, 파일이 바뀌면 그 파일에 의존하는 호출만 다시 실행합니다. Webpack이 모듈과 청크 단위로 캐시하는 것보다 훨씬 잘게 재사용 단위를 나누는 셈입니다.
개발 서버에서의 차이
정확한 차이는 CPU 코어 수, 디스크 속도, 의존성 규모, 소스맵 설정에 따라 달라지므로 자기 프로젝트에서 직접 재 보는 것이 가장 정확합니다. 구조적으로 예상할 수 있는 차이는 다음과 같습니다.
| 관점 | Webpack(개발) | Turbopack(개발) |
|---|---|---|
| 콜드 스타트 | 요청된 페이지의 엔트리 그래프를 JS 로더 체인으로 처리 | 같은 그래프를 Rust에서 병렬 처리하고, 요청된 라우트만 컴파일 |
| HMR | 변경 모듈과 영향받는 청크를 다시 생성 | 변경에 의존하는 함수 호출만 다시 실행 |
| 설정 이식 | webpack() 훅으로 세밀 제어 | Webpack 설정을 읽지 않음, turbopack 옵션으로 재구성 |
Next.js의 Webpack 개발 서버도 요청된 페이지만 컴파일하는 지연 방식(on-demand entries)을 쓰므로, 차이는 “어떤 범위를 컴파일하느냐”보다 “같은 범위를 얼마나 빨리, 얼마나 잘게 재사용하며 처리하느냐”에서 주로 생깁니다.
버전별 Turbopack 활성화
Next.js 버전에 따라 활성화 방법이 다릅니다.
| Next.js | next dev | next build |
|---|---|---|
| 13.x~14.x | --turbo 플래그(베타) | Webpack |
| 15.x | --turbopack 플래그(안정) | Webpack, 15.3 이후 --turbopack은 실험·베타 |
| 16.x 이후 | Turbopack이 기본 | Turbopack이 기본, --webpack으로 Webpack 사용 |
Next.js 15 프로젝트라면 개발 스크립트에 플래그를 고정합니다.
{
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start"
}
}
Next.js 16 이후에는 반대로, 아직 Webpack이 필요한 프로젝트가 next dev --webpack, next build --webpack을 명시합니다. 이 버전에서는 next.config에 webpack() 함수가 있는데 Turbopack으로 빌드하려 하면 설정이 무시되는 대신 빌드가 오류로 멈추므로, 업그레이드 직후 빌드가 깨졌다면 이 부분부터 확인합니다.
설정 키 이름 변화: experimental.turbo에서 turbopack으로
예전에는 experimental.turbo 아래에 옵션을 두었지만, Next.js 15.3부터 최상위 turbopack 키로 옮겨졌고 이전 키는 폐기되었습니다. 기존 프로젝트는 공식 codemod로 옮길 수 있습니다.
npx @next/codemod@latest next-experimental-turbo-to-turbopack .
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
turbopack: {
// rules, resolveAlias, resolveExtensions, root 등
},
}
export default nextConfig
증분 컴파일: 왜 조금 고치면 빠른가
개념
증분 컴파일(incremental compilation)은 매번 전체 프로젝트를 처음부터 빌드하지 않고, 변경된 입력과 그에 의존하는 계산만 다시 하는 방식입니다. TypeScript의 --incremental과 이름은 비슷하지만, 여기서는 번들러의 모듈 그래프와 그 위의 변환 결과를 무효화(invalidation)하는 범위가 관심사입니다.
Turbopack에서의 동작
모듈 그래프는 파일 간 import/export로 연결됩니다. 파일 하나가 바뀌면 Turbopack은 그 파일의 파싱과 변환, 그리고 그 결과에 의존하는 계산만 다시 실행합니다. 수정한 파일의 export 목록이 그대로라면 그 파일을 가져가는 상위 모듈들은 다시 분석할 필요가 없고, 바뀐 모듈의 코드만 HMR 업데이트로 브라우저에 보냅니다. 또한 개발 모드에서는 아직 요청되지 않은 라우트를 컴파일하지 않습니다.
Next.js 16부터는 이 계산 결과를 디스크에 저장해 개발 서버를 다시 켤 때 재사용하는 파일 시스템 캐시가 실험 옵션으로 제공됩니다. 메모리 캐시만 쓸 때는 서버를 재시작하면 처음부터 다시 계산해야 하므로, 대규모 프로젝트에서 재시작 비용이 크다면 해당 버전의 문서에서 옵션 이름과 안정화 여부를 확인해 볼 만합니다.
무효화 범위를 넓히는 구조
증분이 항상 이론상의 최소만 다시 계산한다는 뜻은 아닙니다. 수백 개 모듈을 다시 내보내는 barrel(index.ts) 파일은 하나만 바뀌어도 그것을 가져가는 모든 모듈의 그래프 분석에 영향을 주고, 처음 로드할 때도 실제로 쓰지 않는 모듈까지 그래프에 끌어들입니다. 전역 CSS, 모듈 최상위에서 부수 효과를 일으키는 코드, 자주 바뀌는 공용 상수 파일도 같은 문제를 만듭니다. Turbopack으로 바꿨는데도 느리다면 번들러보다 의존성 구조를 먼저 봐야 합니다.
로더와 플러그인: Webpack과의 대응 관계
Webpack 플러그인은 옮길 수 없다
Webpack의 핵심 확장 수단인 플러그인 API(compiler와 compilation 훅)는 Turbopack에 대응하는 것이 없습니다. webpack(config, { isServer }) 안에서 한 수정은 Turbopack에 적용되지 않으며, 대신 turbopack.rules, resolveAlias, resolveExtensions로 같은 결과를 만들 수 있는지 확인해야 합니다. 플러그인으로만 가능한 일(빌드 산출물 후처리, 커스텀 에셋 생성 등)은 빌드 후 스크립트로 빼는 경우가 많습니다.
지원되는 Webpack 로더
Turbopack은 Webpack 로더 API의 핵심 부분을 구현해 많은 로더를 그대로 실행할 수 있습니다. 공식 문서는 @svgr/webpack, yaml-loader, raw-loader, string-replace-loader, graphql-tag/loader 등을 확인된 로더로 소개합니다. Babel과 Sass, PostCSS는 프로젝트에 설정 파일이 있으면 Next.js가 직접 처리하므로 로더로 등록하지 않습니다. this.importModule처럼 구현되지 않은 로더 API에 의존하는 로더는 동작하지 않을 수 있으므로, 로더별로 실제로 빌드해 보고 확인해야 합니다.
turbopack.rules로 SVGR 적용
/** @type {import('next').NextConfig} */
const nextConfig = {
turbopack: {
rules: {
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
},
}
module.exports = nextConfig
as: '*.js'는 로더의 출력을 .js 파일처럼 취급해 이후 JavaScript 처리 파이프라인으로 넘기라는 뜻입니다. 이 설정이 없으면 SVG 파일의 기본 처리(정적 에셋)가 적용되어 컴포넌트로 import할 수 없습니다. 최근 버전에서는 규칙에 조건(condition)을 붙여 특정 환경에만 로더를 적용할 수 있으므로, 필요하면 사용하는 버전의 문서에서 지원 여부를 확인합니다.
resolveAlias로 패키지 치환
module.exports = {
turbopack: {
resolveAlias: {
underscore: 'lodash',
// 브라우저 번들에서만 대체 구현을 쓰려면 조건부 별칭을 씁니다
fs: { browser: './empty.js' },
},
},
}
Webpack의 resolve.alias나 resolve.fallback과 비슷한 역할입니다. Webpack 5가 Node 내장 모듈 폴리필을 자동으로 넣지 않게 된 뒤 fallback: { fs: false } 같은 설정을 쓰던 프로젝트는, 위처럼 브라우저 조건 별칭으로 옮기는 경우가 많습니다.
모노레포 환경 최적화
모노레포에서 Turbopack이 자주 부딪히는 지점은 프로젝트 루트 밖 파일의 해석과 로컬 패키지의 트랜스파일입니다.
turbopack.root
Turbopack은 루트 디렉터리 밖의 파일을 해석하지 않으며, 이 범위가 파일 감시와 캐시 무효화의 범위이기도 합니다. Next.js는 lockfile 위치 등으로 워크스페이스 루트를 추정하지만, 추정이 틀리거나 lockfile이 여러 개라 경고가 나오면 직접 지정합니다.
const path = require('path')
module.exports = {
turbopack: {
root: path.join(__dirname, '..', '..'), // 예: apps/web에서 워크스페이스 루트로
},
}
npm link나 file: 의존성으로 저장소 밖의 패키지를 연결해 개발할 때는, 그 패키지가 들어 있는 상위 디렉터리까지 포함되도록 root를 넓혀야 합니다. 그만큼 감시 대상 파일이 늘어나므로 개발이 끝나면 되돌리는 편이 낫습니다.
transpilePackages
워크스페이스 안의 패키지가 TypeScript나 최신 문법 소스를 빌드 없이 그대로 내보내고, 앱이 이를 직접 가져다 쓴다면 transpilePackages에 패키지 이름을 적어 Next.js가 그 패키지도 컴파일하게 합니다. Webpack과 Turbopack 모두에서 동작하는 Next.js 설정입니다.
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['@myorg/ui', '@myorg/utils'],
}
module.exports = nextConfig
Turborepo와의 역할 분담
Turborepo는 워크스페이스의 lint, test, build 태스크를 의존 순서대로 실행하고 결과를 로컬·원격에 캐시하는 태스크 러너이고, Turbopack은 Next.js 앱 하나의 모듈을 묶는 번들러입니다. 이름이 비슷해 혼동하기 쉽지만 루트 turbo.json의 태스크 정의와 next dev의 번들러 선택은 서로 다른 층위의 설정입니다. Turborepo가 앱의 build 태스크를 실행하면 그 안에서 Next.js가 Turbopack(또는 Webpack)으로 번들링하는 관계입니다.
마이그레이션 절차
사전 점검
먼저 webpack() 커스텀 설정이 있는지, 있다면 각 수정이 rules나 resolveAlias, Next.js 내장 기능으로 대체되는지 하나씩 확인합니다. 로더는 rules로 옮긴 뒤 실제로 빌드해 동작을 확인합니다. DefinePlugin으로 상수를 주입하고 있었다면 NEXT_PUBLIC_ 접두사 환경 변수나 next.config의 env로 옮깁니다(예전의 publicRuntimeConfig는 폐기 대상이므로 새로 쓰지 않습니다). 커스텀 .babelrc가 있으면 Next.js가 SWC 대신 Babel을 쓰게 되어 속도 이점이 줄어드므로, 정말 필요한 Babel 플러그인인지도 함께 검토합니다.
단계적 롤아웃
개발 서버부터 전환하는 것이 위험이 가장 작습니다. 한두 명이 Turbopack 개발 서버로 일상 개발을 해 보며 HMR 누락이나 스타일 차이를 찾고, 문제가 없으면 팀 스크립트를 바꿉니다. 프로덕션 빌드는 Webpack 빌드와 Turbopack 빌드를 스테이징에서 나란히 띄워 번들 크기, 주요 페이지 동작, CSS 순서 차이를 비교한 뒤 전환합니다. CSS 모듈 간 로드 순서처럼 Webpack에서 우연히 맞던 동작이 달라질 수 있으므로 시각적 회귀 테스트가 있으면 도움이 됩니다.
롤백 계획
이전 스크립트를 함께 남겨 두면 호환성 문제가 발견됐을 때 바로 되돌릴 수 있습니다. 아래는 Next.js 16 이후 기준입니다.
{
"scripts": {
"dev": "next dev",
"dev:webpack": "next dev --webpack",
"build": "next build",
"build:webpack": "next build --webpack"
}
}
트러블슈팅
설정을 바꿨는데 반영되지 않는다면, 수정한 곳이 webpack() 함수가 아니라 turbopack 키인지 확인하고 .next 디렉터리를 지운 뒤 다시 실행합니다. 모노레포 패키지를 찾지 못한다면 turbopack.root, transpilePackages, 그리고 해당 패키지 package.json의 exports 필드가 가져오려는 경로를 노출하는지 확인합니다. 특정 로더만 실패한다면 그 로더가 Turbopack이 구현하지 않은 로더 API를 쓰는지 Next.js 저장소의 이슈를 검색해 봅니다.