본문 바로가기
SPA/Next

Turbopack, Next.js 16 기본 번들러 전환과 webpack 커스텀 설정 마이그레이션

by J4J 2026. 8. 1.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 Next.js 16에서 Turbopack이 기본 번들러로 자리잡은 배경과, 기존 webpack 커스텀 설정을 Turbopack 설정으로 옮기는 방법에 대해 알아보는 시간을 가져보려고 합니다.

 

 

 

Turbopack이 Next.js 16의 기본 번들러가 된 이유

 

Next.js는 오랫동안 webpack을 기본 번들러로 사용해왔고, 프로젝트 규모가 커질수록 dev 서버의 컴파일 속도와 프로덕션 빌드 시간이 체감될 정도로 느려지는 경우가 많았습니다.

 

저의 경우 페이지 수가 많은 프로젝트에서 코드 한 줄을 수정하고 Fast Refresh 결과를 확인하기까지 기다리는 시간이 점점 길어지는 것을 체감한 적이 있습니다.

 

 

 

그러면 Next.js 16은 번들러 문제를 어떻게 해결했을까요?

 

Next.js 16부터는 Rust로 작성된 번들러인 Turbopack이 next dev와 next build 양쪽 모두에서 기본 번들러로 전환되었습니다.

 

Next.js 공식 발표에 따르면 Turbopack은 webpack 대비 프로덕션 빌드에서 2배에서 5배, Fast Refresh에서는 최대 10배 빠른 성능을 보여준다고 합니다.

 

다만 이 수치는 무엇을 비교했는지에 따라 달라지므로, 전체 빌드 시간인지 재시작 후 첫 컴파일 시간인지를 함께 확인하는 것이 좋습니다.

 

 

 

만약 기존 프로젝트의 next.config.js에 webpack() 함수로 커스텀 설정을 해두었다면, Next.js 16으로 올리는 순간 이 설정이 어떻게 되는지 궁금하실 수 있습니다.

 

Turbopack이 기본 번들러가 된 이후로는 webpack() 함수 자체를 인식하지 않으며, 이 상태로 next build를 실행하면 오류가 발생하며 빌드 자체가 중단됩니다.

 

예전처럼 webpack을 그대로 사용하고 싶다면 다음과 같이 --webpack 플래그를 붙여 실행할 수 있습니다.

 

// terminal
$ next dev --webpack
$ next build --webpack

 

그러므로 Module Federation처럼 webpack 플러그인에 의존하는 특별한 사정이 없다면, 플래그 없이 Turbopack을 그대로 사용하는 것이 좋습니다.

 

다음으로는 기존 webpack 커스텀 설정을 실제로 Turbopack 설정으로 옮기는 방법을 살펴보겠습니다.

 

 

반응형

 

 

webpack 커스텀 설정을 Turbopack 설정으로 옮기기

 

Turbopack으로 전환한 뒤 가장 먼저 마주치는 질문은 기존 webpack() 함수 안에 있던 설정을 어떻게 그대로 옮기는가입니다.

 

먼저 SVG 파일을 React 컴포넌트로 변환해주는 @svgr/webpack loader를 webpack 방식으로 설정한 예시부터 보겠습니다.

 

// ❌ next.config.ts, webpack() 함수로 SVG loader를 설정하는 기존 방식
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  webpack(config) {
    config.module.rules.push({
      test: /\.svg$/,
      use: ['@svgr/webpack'],
    })

    return config
  },
}

export default nextConfig

 

이렇게 작성하면 Next.js 16에서는 webpack() 함수 자체를 인식하지 않으며, turbopack 설정 없이 이 상태로 next build를 실행하면 SVG 변환만 빠지는 것이 아니라 빌드 자체가 오류로 중단됩니다.

 

 

 

turbopack.rules는 특정 파일 패턴에 어떤 loader를 적용할지 지정하는 설정이며, webpack의 module.rules와 동일한 역할을 담당합니다.

 

// ✅ next.config.ts, turbopack.rules로 옮긴 방식
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  turbopack: {
    rules: {
      '*.svg': {
        loaders: ['@svgr/webpack'],
        as: '*.js',
      },
    },
  },
}

export default nextConfig

 

그러므로 module.rules에 넣던 loader 설정은 turbopack.rules에 글롭 패턴을 키로 하는 객체 형태로 옮기는 것이 좋습니다.

 

다만 turbopack.rules는 코드를 다른 JS 코드로 변환하는 loader만 지원하며, 결과물의 형식을 as 값으로 직접 명시해야 한다는 점이 webpack loader와 다릅니다.

 

 

 

이번에는 경로 alias 설정을 옮겨보겠습니다.

 

// ❌ next.config.ts, webpack alias로 경로를 설정하는 기존 방식
import path from 'path'
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  webpack(config) {
    config.resolve.alias = {
      ...config.resolve.alias,
      '@ui': path.resolve(__dirname, 'src/ui'),
    }

    return config
  },
}

export default nextConfig

 

이렇게 작성하면 이 alias 설정도 마찬가지로 webpack() 함수 자체가 인식되지 않아 next build 시 오류가 발생합니다.

 

 

 

resolveAlias는 import 경로를 다른 경로로 매핑하는 설정이며, webpack의 resolve.alias와 동일한 역할을 담당합니다.

 

// ✅ next.config.ts, turbopack.resolveAlias로 옮긴 방식
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  turbopack: {
    resolveAlias: {
      '@ui': './src/ui',
    },
  },
}

export default nextConfig

 

resolveAlias는 path.resolve 없이 프로젝트 루트 기준 상대 경로 문자열만 넘기면 되기 때문에 기존 webpack alias보다 오히려 간결해집니다.

 

다만 resolveAlias의 조건부 aliasing은 browser 조건 하나만 지원하기 때문에, node나 server 조건별로 다른 모듈을 연결하던 설정이 있었다면 그대로 옮겨지지 않는다는 점은 알아두는 것이 좋습니다.

 

 

 

 

또한 위 예시처럼 와일드카드 없이 '@ui' 하나만 등록하면 정확히 '@ui'라는 import 경로만 별칭 처리되며, webpack alias와 달리 '@ui/Button'처럼 하위 경로를 가진 import는 자동으로 매칭되지 않습니다.

 

// ❌ next.config.ts, 하위 경로 import까지 자동으로 매칭될 것으로 기대하는 경우
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  turbopack: {
    resolveAlias: {
      '@ui': './src/ui',
    },
  },
}

export default nextConfig

 

이 상태에서 '@ui/Button'을 import하면 Turbopack이 Module not found: Can't resolve '@ui/Button' 오류를 그대로 냅니다.

 

 

 

// ✅ next.config.ts, 하위 경로까지 별칭 처리하는 방식
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  turbopack: {
    resolveAlias: {
      '@ui/*': './src/ui/*',
    },
  },
}

export default nextConfig

 

그러므로 하위 경로까지 하나의 별칭으로 묶고 싶다면 키와 값 양쪽에 와일드카드(*)를 붙이는 것이 좋습니다.

 

추가적으로 Typescript를 사용 하는 프로젝트에서는 주의사항이 존재합니다.

 

resolveAlias는 Turbopack 번들러 레벨에서만 동작하며 TypeScript 타입 체커는 이 설정을 알지 못하기 때문에, tsconfig.json의 paths에 동일한 경로를 등록해두지 않으면 next build의 TypeScript 검사 단계에서 Cannot find module 오류가 발생하며 빌드 자체가 실패합니다.

 

// tsconfig.json, resolveAlias와 동일한 경로를 paths에도 등록
{
  "compilerOptions": {
    "paths": {
      "@ui/*": ["./src/ui/*"]
    }
  }
}

 

그러므로 resolveAlias로 별칭을 추가할 때는 next.config.ts와 tsconfig.json 양쪽에 같은 경로를 함께 등록해두는 것이 좋습니다.

 

 

 

확장자 설정을 옮길 때는 조금 더 주의가 필요합니다.

 

resolveExtensions는 import 시 확장자를 생략했을 때 어떤 확장자를 순서대로 탐색할지 지정하는 설정이며, webpack의 resolve.extensions와 동일한 역할을 담당합니다.

 

// ❌ next.config.ts, mdx 확장자 하나만 추가하려는 경우
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  turbopack: {
    resolveExtensions: ['.mdx'],
  },
}

export default nextConfig

 

이렇게 작성하면 .mdx 파일은 인식되지만, resolveExtensions가 넘긴 배열이 기존 확장자 목록 전체를 덮어써 버려서 .tsx, .ts, .js 같은 기본 확장자 해석이 함께 깨집니다.

 

 

 

// ✅ next.config.ts, 기본 확장자를 모두 포함해 추가하는 방식
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  turbopack: {
    resolveExtensions: ['.mdx', '.tsx', '.ts', '.jsx', '.js', '.mjs', '.json'],
  },
}

export default nextConfig

 

그러므로 resolveExtensions에 확장자를 추가할 때는 새로 추가하는 확장자뿐 아니라 기존에 사용하던 확장자까지 모두 나열하는 것이 좋습니다.

 

 

 

 

마지막으로 특정 패키지를 번들링 대상에서 제외하는 externals 설정입니다.

 

webpack에서는 config.externals에 패키지를 추가해 번들링 대상에서 제외했지만, Turbopack에는 이 externals에 정확히 대응하는 설정 키가 없습니다.

 

// ❌ next.config.ts, webpack externals로 네이티브 모듈을 제외하는 기존 방식
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  webpack(config) {
    config.externals.push('sharp')

    return config
  },
}

export default nextConfig

 

이렇게 작성하면 이 externals 설정도 마찬가지로 webpack() 함수 자체가 인식되지 않아 next build 시 오류가 발생합니다.

 

 

 

serverExternalPackages는 지정한 패키지를 서버 번들에서 제외하고 런타임에 Node.js의 require로 그대로 불러오도록 지정하는 설정입니다.

 

// ✅ next.config.ts, serverExternalPackages로 대체하는 방식
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  serverExternalPackages: ['sharp'],
}

export default nextConfig

 

serverExternalPackages는 turbopack 전용 키가 아니라 Next.js가 번들러와 무관하게 제공하는 공통 옵션입니다.

 

그리고 Next.js 16.1부터는 직접 의존성뿐 아니라 간접 의존성까지 자동으로 externalize하도록 개선되었습니다.

 

다음으로는 이렇게 Turbopack으로 옮긴 뒤에도 아직 남아있는 제약과, 그럼에도 전환하는 것이 좋은 상황을 정리해 보겠습니다.

 

 

 

 

Turbopack으로 전환하면 좋은 상황과 아직 남아있는 제약

 

지금까지 살펴본 것처럼 loader, alias, 확장자, externals 설정은 대부분 Turbopack으로 옮길 수 있지만, 구조적으로 아예 옮길 수 없는 기능들도 남아 있습니다.

 

가장 크게 부딪히는 지점은 webpack 플러그인입니다.

 

Turbopack은 loader는 지원하지만 webpack 플러그인은 지원하지 않으며, 이에 의존하는 도구는 Turbopack과 호환되는 대안을 찾거나 동등한 기능이 제공되기 전까지 webpack을 유지해야 한다고 공식 문서에 명시되어 있습니다.

 

그러므로 Module Federation처럼 플러그인 형태로만 제공되는 기능을 쓰고 있다면 해당 프로젝트는 당분간 webpack을 유지하는 것이 좋습니다.

 

 

 

이 외에도 다음과 같은 경우들은 Next.js 16 시점에서 아직 Turbopack으로 온전히 옮기기 어렵습니다.

 

  • sassOptions.functions로 등록한 커스텀 Sass 함수 (Rust 기반 구조상 JS 함수를 직접 실행할 수 없어 미지원)
  • Sass의 legacy tilde(~) import 문법 (resolveAlias에 '~*': '*' 규칙을 추가하거나 import 경로 자체를 고쳐야 함)
  • 네이티브 바인딩이 없는 FreeBSD, OpenBSD 같은 일부 플랫폼 (WASM 폴백이 Turbopack을 지원하지 않아 --webpack이 사실상 필수)

 

 

 

반대로 다음과 같은 상황이라면 Turbopack으로 전환하는 것이 좋습니다.

 

  • 위에서 언급한 webpack 플러그인이나 Sass 커스텀 함수처럼 구조적으로 막히는 기능을 쓰고 있지 않은 경우
  • dev 서버 재시작, Fast Refresh 속도를 실질적으로 체감하고 싶은 경우
  • 신규 프로젝트를 시작하는 경우 (Next.js 16부터는 별도 설정 없이 Turbopack이 기본이므로 그대로 사용하는 것이 좋습니다)

 

그러면 Turbopack으로 전환한 뒤 dev 서버 속도를 한 단계 더 끌어올릴 수 있는 Persistent Caching을 살펴보겠습니다.

 

 

 

 

Persistent Caching으로 dev 서버 재시작 속도 개선하기

 

Turbopack은 기본적으로 dev 서버를 재시작하면 이전 컴파일 결과를 다시 활용하지 못하고 처음부터 다시 컴파일합니다.

 

이 문제를 해결하기 위해 등장한 기능이 파일시스템 기반 Persistent Caching이며, dev 환경과 build 환경에서 각각 다른 안정성 단계로 제공되고 있습니다.

 

dev 환경의 Persistent Caching은 Next.js 16.1부터 정식(stable) 기능으로 전환되어 별도 설정 없이 기본으로 켜져 있습니다.

 

공식 발표에 따르면 이 캐시가 웜(warm) 상태일 때 dev 서버를 재시작한 뒤 첫 라우트를 컴파일하는 시간이, nextjs.org 기준 약 5배(3.5초에서 700ms로), 규모가 큰 사내 애플리케이션 기준으로는 최대 14배(15초에서 1.1초로)까지 단축되었다고 합니다.

 

체감할 수 있는 것 중 하나는, 코드를 조금 고치고 dev 서버를 껐다 켜는 일이 잦은 개발 흐름일수록 이 캐시의 효과를 더 크게 느낄 수 있다는 점입니다.

 

 

 

반면 build 환경의 Persistent Caching은 Next.js 16.2 시점까지도 experimental 상태이며 기본값은 꺼져 있습니다.

 

turbopackFileSystemCacheForBuild는 build 환경에서도 파일시스템 기반 Persistent Caching을 사용하도록 켜는 실험적 옵션입니다.

 

이 기능을 사용해보려면 다음과 같이 next.config.ts에 실험적 옵션을 직접 켜는 것이 좋습니다.

 

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  experimental: {
    turbopackFileSystemCacheForBuild: true,
  },
}

export default nextConfig

 

experimental 옵션이라는 이름 그대로, 아직 프로덕션 빌드 파이프라인에 전면 도입하기보다는 로컬이나 CI 빌드 속도를 실험적으로 확인해보는 용도로 먼저 사용하는 것이 좋습니다.

 

Turbopack과 webpack의 빌드 속도를 직접 비교해볼 때는 매번 .next 폴더를 지우고 콜드 빌드끼리 비교하거나, 캐시를 켠 뒤 웜 빌드끼리 비교해야 공정한 비교가 됩니다.

 

그렇지 않으면 캐시 유무 차이를 번들러 자체의 성능 차이로 오인하기 쉽습니다.

 

 

 

 

지금까지 살펴본 turbopack.rules, resolveAlias, resolveExtensions, serverExternalPackages, experimental 옵션을 하나의 next.config.ts로 합쳐보면 다음과 같습니다.

 

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  turbopack: {
    rules: {
      '*.svg': {
        loaders: ['@svgr/webpack'],
        as: '*.js',
      },
    },
    resolveAlias: {
      '@ui/*': './src/ui/*',
    },
    resolveExtensions: ['.mdx', '.tsx', '.ts', '.jsx', '.js', '.mjs', '.json'],
  },
  serverExternalPackages: ['sharp'],
  experimental: {
    turbopackFileSystemCacheForBuild: true,
  },
}

export default nextConfig

 

 

 

 

 

 

 

이상으로 Next.js 16에서 Turbopack이 기본 번들러로 자리잡은 배경과 webpack 커스텀 설정을 옮기는 방법에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글