Webpack에서 Rspack으로 옮기기: Loader·Plugin 호환성, SWC 설정, React·Vue 구성, 벤치마크
이 글의 핵심
기존 Webpack 설정을 Rspack으로 옮길 때 무엇이 그대로 되고 무엇을 바꿔야 하는지, SWC 기반 트랜스파일 설정, React·Vue 프로젝트 구성, 성능 수치를 해석하는 법을 정리합니다.
들어가며
Rspack은 Rust로 구현된 모듈 번들러입니다. 다른 신형 번들러와 달리 Webpack의 설정 형식과 Loader·Plugin API를 의도적으로 따라가기 때문에, 기존 webpack.config.js를 크게 고치지 않고 옮겨 올 수 있다는 점이 가장 큰 특징입니다. 다만 동작이 100% 같지는 않아서, 마이그레이션할 때는 무엇이 다른지 목록을 만들고 회귀 테스트로 확인하는 과정이 필요합니다.
이 글은 Webpack에서 Rspack으로 옮기는 절차, Loader와 Plugin 호환성, builtin:swc-loader 설정, React·Vue 프로젝트 구성, 벤치마크를 해석하는 방법, Turbopack·Rsbuild와의 관계를 순서대로 다룹니다. 모듈 해석, entry/output, Loader 체인, Plugin 훅 같은 Webpack 개념을 알고 있다고 가정합니다.
Rspack이란 무엇인가
설계 목표
Rspack은 Webpack의 module.rules, plugins, resolve 같은 설정 개념을 그대로 유지하면서, 모듈 그래프 구성·코드 변환·청크 분할·최소화 같은 무거운 작업을 Rust 코어에서 병렬로 처리합니다. JavaScript·TypeScript 변환은 SWC를 내장한 builtin:swc-loader가 맡습니다.
새로운 철학의 도구라기보다 Webpack의 모델을 유지하고 구현을 바꾼 번들러에 가깝습니다. 개발 서버에서 네이티브 ESM을 그대로 서빙하는 Vite나, Next.js 안에 내장된 Turbopack과는 해결하려는 문제가 다릅니다.
구성 요소
| 개념 | 설명 |
|---|---|
| 컴파일 파이프라인 | Webpack과 같은 방식으로 모듈 그래프를 만들고 청크로 나눕니다. |
| Loader | 파일을 모듈로 변환하는 함수 체인입니다. 많은 Webpack Loader가 그대로 동작합니다. |
| Plugin | 컴파일 라이프사이클 훅에 개입합니다. 자주 쓰는 Webpack 플러그인은 Rust로 구현된 내장 버전이 있습니다. |
| 내장 SWC | builtin:swc-loader가 Babel(트랜스파일)을, 내장 최소화 플러그인이 Terser를 대신합니다. |
| Dev Server | @rspack/dev-server가 개발 서버와 HMR을 담당합니다. |
JavaScript로 작성된 Loader를 쓰면 파일 내용을 Rust 코어와 Node.js 사이에서 주고받는 비용이 생기고, 그 Loader 자체는 JavaScript 속도로 돌아갑니다. Rspack으로 옮기고도 빌드가 기대만큼 빨라지지 않는다면 대개 이 경계에서 시간이 쓰이고 있으므로, 내장 Loader나 내장 Plugin으로 바꿀 수 있는 것부터 바꾸는 것이 효과적입니다.
설치와 기본 프로젝트 뼈대
패키지 구성
npm add -D @rspack/core @rspack/cli @rspack/dev-server
@rspack/core가 번들러 본체이고, @rspack/cli는 rspack build, rspack dev 명령을 제공하며, @rspack/dev-server는 개발 서버를 담당합니다.
설정 파일 이름
Webpack의 webpack.config.js에 해당하는 파일은 rspack.config.js(또는 .mjs, .ts)입니다. 메이저 버전이 바뀔 때 지원하는 Node.js 최소 버전과 모듈 형식(ESM 전환 등)이 함께 바뀌는 경우가 있으므로, 업그레이드 전에 릴리스 노트에서 요구 사항을 확인해야 합니다.
npm 스크립트
{
"scripts": {
"dev": "rspack dev",
"build": "rspack build",
"preview": "rspack preview"
}
}
기존의 webpack serve, webpack --mode production 스크립트를 이렇게 바꿉니다. 영속 캐시를 켰다면 캐시 디렉터리(기본적으로 node_modules/.cache 아래)를 CI에서 보존해야 반복 빌드에서 효과를 볼 수 있습니다.
Webpack에서 마이그레이션하기
절차
- 사용 중인 Loader, Plugin,
resolve.alias, 환경 변수 주입 방식을 목록으로 정리합니다. entry와output만 맞춘 최소 설정으로rspack dev를 띄워 애플리케이션이 기동하는지 확인합니다.module.rules를 파일 형식별로 하나씩 옮기면서 그때마다 화면과 테스트로 확인합니다.- 이름이 바뀐 플러그인을 공식 마이그레이션 가이드와 대조해 교체합니다.
optimization.splitChunks,usedExports같은 프로덕션 전용 옵션을 맞추고, Webpack 빌드와 산출물 크기·청크 구성을 비교합니다.
한 번에 전부 옮기는 큰 브랜치보다, 단계마다 머지할 수 있는 작은 변경으로 나누는 편이 문제를 찾기 쉽습니다.
자주 만나는 치환
| Webpack | Rspack |
|---|---|
webpack.DefinePlugin (webpack 패키지) | rspack.DefinePlugin (@rspack/core) |
copy-webpack-plugin | rspack.CopyRspackPlugin |
html-webpack-plugin | rspack.HtmlRspackPlugin (또는 html-webpack-plugin 그대로) |
mini-css-extract-plugin | rspack.CssExtractRspackPlugin |
babel-loader | builtin:swc-loader |
terser-webpack-plugin | rspack.SwcJsMinimizerRspackPlugin (프로덕션 기본값) |
webpack-bundle-analyzer | Rsdoctor |
html-webpack-plugin처럼 원래 플러그인이 그대로 동작하는 경우도 있지만, 내장 버전은 Rust로 구현되어 JavaScript 경계를 넘지 않으므로 대체로 더 빠릅니다. Webpack 설정에 직접 작성한 커스텀 Plugin이 깊이 들어가 있다면, 그 플러그인이 쓰는 훅을 Rspack이 지원하는지부터 확인해야 합니다. Rspack은 Webpack의 훅을 대부분 제공하지만 전부는 아니고, 같은 훅이라도 내부 객체의 세부 동작이 다를 수 있습니다.
엄격해진 기본값
메이저 버전 업그레이드에서 파서 기본값이 엄격해지는 경우가 있습니다. 예를 들어 존재하지 않는 export를 import할 때의 처리(module.parser.javascript.exportsPresence)를 경고가 아니라 에러로 바꾸면, 마이그레이션 직후 그동안 경고로 묻혀 있던 잘못된 import가 한꺼번에 빌드 에러로 드러납니다. 이 경우 옵션을 일시적으로 'warn'으로 낮춰 두고 코드를 정리한 뒤 다시 엄격하게 되돌리는 방법이 있습니다. 사용하는 버전의 기본값은 릴리스 노트에서 확인하세요.
Loader와 Plugin 호환성
Loader
Rspack은 Webpack의 Loader API를 구현하므로, 문자열을 받아 문자열을 돌려주는 일반적인 Loader는 대부분 그대로 동작합니다. 다음 경우는 따로 확인하는 것이 좋습니다.
this.cacheable(),this.async()같은 Loader 컨텍스트 API를 특이하게 쓰는 Loader- pitch 단계에 의존하는 Loader(
style-loader처럼 pitch에서 요청을 가로채는 방식) - 네이티브 애드온을 쓰는 Loader의 플랫폼 호환성
스타일 파이프라인은 프로젝트마다 조합이 달라 문제가 가장 많이 나는 부분입니다. CSS Modules, Sass, PostCSS 조합을 작은 샘플 프로젝트에서 먼저 재현해 보면 본 프로젝트에서의 시행착오를 줄일 수 있습니다.
Plugin
커뮤니티 Webpack Plugin은 그대로 쓸 수 있는 것과 Rspack용 대체재가 필요한 것으로 나뉩니다. 컴파일 결과를 읽기만 하는 플러그인은 대체로 동작하고, 청크 ID 생성이나 모듈 그래프 내부 구조에 의존하는 플러그인은 깨지기 쉽습니다. 공식 문서의 플러그인 호환성 목록에서 사용하는 플러그인을 먼저 찾아보고, 목록에 없는 것은 빌드 산출물을 Webpack 결과와 비교해 검증합니다.
SWC 통합
builtin:swc-loader는 JavaScript 패키지인 swc-loader와 같은 옵션 형식을 쓰지만, Rust 코어 안에서 직접 실행되므로 파일 내용을 Node.js 쪽으로 넘겼다가 다시 받는 과정이 없습니다.
TypeScript 처리
export default {
module: {
rules: [
{
test: /\.ts$/,
exclude: [/node_modules/],
loader: 'builtin:swc-loader',
options: {
jsc: {
parser: {
syntax: 'typescript',
},
},
},
type: 'javascript/auto',
},
],
},
};
type: 'javascript/auto'는 Loader가 출력한 결과를 ESM과 CommonJS를 모두 허용하는 JavaScript 모듈로 처리하라고 번들러에 알려 줍니다. SWC는 타입을 지우기만 하고 타입 검사는 하지 않으므로, tsc --noEmit이나 fork-ts-checker 같은 별도 검사를 CI에 두어야 합니다. 파일 단위로 변환하기 때문에 tsconfig.json에 isolatedModules: true를 켜 두면 SWC가 처리할 수 없는 코드(타입만 다시 export하는 구문 등)를 미리 잡을 수 있습니다.
JSX와 React
JSX를 쓰면 jsc.parser에 jsx: true(TypeScript라면 tsx: true)를 주고, jsc.transform.react에서 런타임과 개발 모드 여부를 지정합니다. React Fast Refresh는 아래 React 설정 절에서 다룹니다.
타깃과 폴리필
jsc.target과 env.targets는 함께 설정할 수 없습니다. 브라우저 범위를 browserslist로 관리한다면 env.targets를, 특정 ES 버전으로만 낮추면 되는 라이브러리라면 jsc.target을 씁니다.
env.mode: 'usage'로 필요한 core-js 폴리필을 자동 주입할 때는 core-js 패키지 자체를 SWC 처리 대상에서 제외해야 합니다. 그러지 않으면 core-js 내부 코드에 다시 폴리필 import가 주입되어 순환 참조가 생기고 런타임에서 오동작할 수 있습니다.
babel-plugin-import 대체
Ant Design 등에서 쓰던 babel-plugin-import 방식의 개별 import 변환은 builtin:swc-loader의 옵션으로 대체할 수 있습니다(초기 버전의 rspackExperiments.import, 이후 버전에서는 transformImport). Babel 플러그인처럼 customName에 함수를 넘기는 대신 템플릿 문자열을 쓰도록 설계되어 있어 Rust 쪽에서 처리할 수 있습니다. 함수로만 표현되는 복잡한 규칙을 쓰고 있었다면 템플릿으로 옮길 수 있는지 먼저 확인해야 합니다.
TypeScript 정보 수집 옵션
collectTypeScriptInfo 같은 실험적 옵션은 SWC가 변환하면서 얻은 TypeScript 정보(타입만 다시 export하는지, const enum 값 등)를 번들러의 후속 최적화에 넘겨 줍니다. 이 정보는 SWC가 본 코드 기준이므로, 체인에서 SWC 다음에 export 구조를 바꾸는 Loader가 있으면 정보가 실제 코드와 어긋날 수 있습니다.
React 프로젝트 설정
React 개발 환경의 Fast Refresh는 @rspack/plugin-react-refresh 플러그인과 SWC의 refresh 변환을 함께 써서 구성합니다. 둘 다 개발 모드에서만 켜야 하고, 경로 별칭(resolve.alias)은 tsconfig.json의 paths와 같은 값을 유지해야 에디터와 빌드 결과가 어긋나지 않습니다.
import { defineConfig } from '@rspack/cli';
import ReactRefreshPlugin from '@rspack/plugin-react-refresh';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const isDev = process.env.NODE_ENV === 'development';
export default defineConfig({
entry: { main: './src/index.tsx' },
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
module: {
rules: [
{
test: /\.(j|t)sx?$/,
exclude: [/node_modules/],
use: {
loader: 'builtin:swc-loader',
options: {
jsc: {
parser: { syntax: 'typescript', tsx: true },
transform: {
react: {
runtime: 'automatic',
development: isDev,
refresh: isDev,
},
},
},
},
},
type: 'javascript/auto',
},
],
},
plugins: [isDev && new ReactRefreshPlugin()].filter(Boolean),
});
서버 컴포넌트와 서버 렌더링까지 포함한 React 앱은 보통 Next.js 같은 프레임워크가 번들링을 맡습니다. Rspack을 직접 구성하는 것은 클라이언트 렌더링 SPA이거나 자체 SSR 파이프라인을 운영하는 경우가 대부분입니다.
Vue 프로젝트 설정
Vue 3 단일 파일 컴포넌트(SFC)는 Webpack 때와 마찬가지로 vue-loader와 VueLoaderPlugin으로 처리합니다. Rspack 공식 가이드의 예제는 다음과 같은 형태입니다.
const { VueLoaderPlugin } = require('vue-loader');
module.exports = {
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
// Rspack에서 vue-loader가 SFC 블록을 처리할 때 필요한 옵션
experimentalInlineMatchResource: true,
},
},
],
},
plugins: [new VueLoaderPlugin()],
};
vue-loader 버전은 Vue 버전과 맞아야 합니다(Vue 3에는 v16 이상, Rspack 가이드는 v17 이상을 기준으로 합니다). <style lang="scss"> 같은 블록은 해당 확장자에 대한 규칙(sass-loader 등)이 있어야 처리되므로, Webpack 설정에 있던 스타일 규칙도 함께 옮겨야 합니다.
새 Vue 프로젝트라면 Vite가 더 일반적인 선택이지만, 이미 Webpack 기반으로 운영 중인 Vue 앱을 빠르게 하고 싶다면 설정 대부분을 유지할 수 있는 Rspack이 현실적인 경로입니다.
성능 벤치마크: 숫자를 읽는 법
결과가 제각각인 이유
공개된 벤치마크에서 Rspack은 Webpack보다 콜드 빌드, 증분 빌드, HMR 모두 빠르게 나오는 경우가 많습니다. 하지만 그 차이는 프로젝트의 모듈 수, 캐시 상태, Loader 구성, 디스크 속도, CI 머신의 코어 수에 크게 좌우됩니다. 특히 JavaScript Loader나 Plugin이 많이 남아 있으면 그 부분은 여전히 JavaScript 속도로 돌기 때문에 격차가 줄어듭니다. 다른 프로젝트의 수치를 그대로 기대하기보다 자기 저장소에서 직접 재는 것이 유일하게 믿을 만한 방법입니다.
비교할 때 맞출 조건
- 같은 Node.js 버전, OS, CPU, 전원 설정
- 캐시를 비운 상태와 캐시가 있는 상태를 각각 따로 측정
- 같은 프로덕션 최적화 옵션(최소화, tree-shaking)
- 같은 소스맵 종류와 코드 분할 설정
소스맵 종류 하나만 달라도 빌드 시간이 크게 달라지므로, Webpack 설정의 devtool을 그대로 옮겼는지 확인한 뒤 비교해야 합니다. 빌드 시간만 보지 말고 산출물 크기와 청크 수도 함께 비교하면, 최소화나 tree-shaking 결과가 달라서 생긴 차이를 놓치지 않습니다.
Webpack vs Rspack vs Turbopack
| 기준 | Webpack | Rspack | Turbopack |
|---|---|---|---|
| 형태 | 범용 번들러 | 범용 번들러 | Next.js 내장 번들러 |
| 설정 모델 | webpack.config.js | Webpack과 거의 같은 설정 | Next.js 설정(next.config)의 일부 |
| 생태계 | 가장 넓음 | Webpack Loader·Plugin 상당수 재사용 | Next.js가 지원하는 범위 |
| 단독 사용 | 가능 | 가능 | Next.js 밖에서는 사용 불가 |
Webpack은 여전히 가장 넓은 호환성을 가진 기준점이고, Rspack은 그 설정을 유지하면서 빌드 속도를 높이려는 선택지입니다. Turbopack은 독립 실행 번들러로 배포되지 않고 Next.js 안에서만 쓰이므로, Next.js 앱이 아니라면 비교 대상이 되지 않습니다. Next.js 앱이라면 번들러 선택은 사실상 Next.js가 정해 주는 경로를 따르게 됩니다.
Rspack을 직접 쓸까, Rsbuild를 쓸까
이 글은 기존 Webpack 설정을 옮기는 관점이라 rspack.config를 직접 다뤘지만, 새 프로젝트라면 Rsbuild부터 보는 편이 낫습니다. Rsbuild는 Rspack 위에 올린 빌드 도구로, Vite처럼 TypeScript·JSX·CSS Modules·PostCSS·Sass 처리와 dev 서버·HMR을 기본값으로 묶어 줍니다. Loader 체인을 직접 나열하는 대신 플러그인 하나로 끝나지만, 세밀한 제어가 필요하면 tools.rspack으로 내부 설정에 손을 넣어야 합니다. 기존 Webpack 설정에 커스텀 Loader·Plugin이 많이 얽혀 있으면 Rspack 직접 사용으로 1:1 이전하고, 그렇지 않거나 CRA·자체 템플릿에서 벗어나려는 경우라면 Rsbuild로 가는 쪽이 유지보수 부담이 작습니다.
// rsbuild.config.ts
import { defineConfig } from "@rsbuild/core"
import { pluginReact } from "@rsbuild/plugin-react"
export default defineConfig({
plugins: [pluginReact()],
source: { entry: { index: "./src/index.tsx" } },
server: { port: 3000 },
})
같은 팀이 만드는 주변 도구로는 라이브러리 빌드용 Rslib(ESM·CJS 다중 포맷과 .d.ts 생성을 한 설정에서 처리)과 문서 사이트 생성기 Rspress가 있습니다. 빌드 파이프라인을 한 계열로 맞추면 SWC 설정과 플러그인 지식을 재사용할 수 있습니다.
Module Federation
마이크로 프런트엔드에서 Rspack이 자주 언급되는 이유는 Module Federation 2.0을 지원하기 때문입니다. @module-federation/enhanced의 Rspack 플러그인을 쓰면 remote를 mf-manifest.json 기반으로 불러와 배포 시점에 remote 주소를 바꾸기 쉽고, 원격 모듈의 타입도 생성해 줍니다.
const { ModuleFederationPlugin } = require("@module-federation/enhanced/rspack")
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "host",
remotes: { app1: "app1@http://localhost:3001/mf-manifest.json" },
shared: { react: { singleton: true }, "react-dom": { singleton: true } },
}),
],
}
Module Federation에서 흔한 함정은 shared에 singleton을 빠뜨리는 것입니다. host와 remote가 React를 각자 번들하면 화면은 뜨지만 remote 컴포넌트에서 훅을 쓰는 순간 “Invalid hook call” 에러가 납니다. React는 훅 상태를 모듈 인스턴스에 보관하므로 React 사본이 두 개면 훅이 엉뚱한 인스턴스를 보게 되는데, 에러 메시지는 컴포넌트 코드에 문제가 있는 것처럼 보여 원인을 찾기 어렵습니다. host와 remote의 @module-federation/enhanced 버전을 맞추는 것도 런타임 오류를 줄이는 기본입니다.
트러블슈팅
- SWC Wasm 플러그인은 특정 SWC 코어 버전에 맞춰 빌드되므로, Rspack에 내장된 SWC 버전과 맞지 않으면 로드에 실패하거나 잘못된 코드를 낼 수 있습니다. 공식 문서의 버전 대응표를 따라야 합니다.
- Babel 전용 플러그인(매크로 등)에 대체재가 없다면
babel-loader를 그 플러그인이 필요한 파일 범위에만 남겨 병행합니다. 전체 파일에 Babel을 다시 걸면 속도 이점이 대부분 사라집니다. - 빌드는 되는데 배포 스크립트가 깨진다면 산출물이 Webpack과 다르기 때문인 경우가 많습니다. tree-shaking과 청크 분할 결과가 같지 않아서, 청크 이름이나 파일 해시에 의존하던 캐시 키·배포 스크립트가 어긋날 수 있습니다.
optimization.splitChunks와chunkIds를 명시해 두면 두 번들러의 산출물을 비교하기 쉬워집니다. - 번들 분석을
webpack-bundle-analyzer나 CLI 옵션에 의존하던 파이프라인은 Rsdoctor 기반으로 스크립트를 바꿔야 합니다.