Back

Vite vs. Webpack 2026: 마이그레이션 실전 가이드 (성능 비교 포함)

개발 서버 켜는데 30초, 코드 한 줄 고쳤는데 HMR 반영까지 5초. 혹시 이거 내 얘기인가요?

걱정 마세요, 여러분만 그런 거 아닙니다. 그런데 솔직히 말하면, 2026년에 이러고 있으면 뭔가 놓치고 있는 거예요.

JavaScript 번들러 판도가 완전히 바뀌었거든요. 한 10년 가까이 절대 강자였던 Webpack이 이제는 Vite랑 자리를 나눠 앉게 됐어요. Vite는 "개발할 때 거의 즉시 반응한다"고 약속하고, 진짜로 그렇게 동작하죠. 근데 여기서 중요한 건요, "Vite가 빠르다"만 가지고 선택할 문제가 아니라는 거예요.

이 글에서는 두 도구가 구조적으로 어떻게 다른지 파헤치고, Vite가 그렇게 빠른지, 그리고 Webpack이 아직 더 나은 상황은 언제인지 살펴볼게요. 물론 실제 마이그레이션 가이드까지 전부요.

왜 지금 이 비교가 중요할까?

본격적인 기술 얘기 전에, 2026년 기준으로 왜 이게 더 중요해졌는지 짚어볼게요.

프론트엔드 개발이 달라졌어요

생태계가 확 바뀌었습니다:

  1. ES 모듈이 대세가 됨: 모던 브라우저 전부 네이티브 ES 모듈 지원해요. 개발 환경에서 JS 제공하는 방식 자체가 달라졌죠
  2. TypeScript가 기본: 전문 JS 개발자 78% 이상이 TS 쓰고 있어요. 빌드 도구가 .ts, .tsx 잘 처리해야 하는 시대
  3. DX가 경쟁력: 기업들도 개발자 생산성이 제품 속도에 직결된다는 걸 알게 됐어요
  4. 빌드 시간 = 고통: 프로젝트 커질수록 HMR 100ms와 10초 차이가 하루에 몇 시간씩 쌓여요

느린 빌드가 얼마나 비싼지 아세요?

한번 계산해볼까요. 개발자 50명인 중간 규모 React 앱 기준으로:

지표Webpack (콜드 스타트)Vite (콜드 스타트)
개발 서버 시작45초400ms
HMR3-5초50-200ms
프로덕션 빌드2-3분20-40초

개발자마다 하루에 서버 4번 켜고, 코드 100번 수정한다고 치면:

  • Webpack: 45초 × 4 + 3초 × 100 = 8분/일 대기
  • Vite: 0.4초 × 4 + 0.1초 × 100 = 12초/일 대기

1년(250일) 기준으로:

  • Webpack: 8분 × 50명 × 250일 = 16,667시간 날림
  • Vite: 0.2분 × 50명 × 250일 = 417시간 날림

16,250시간 절약이에요. 풀타임 개발자 8명이 1년 내내 멍때리는 시간이랑 같아요.

구조가 어떻게 다른지 알아야 해요

Webpack: "일단 다 묶어" 방식

Webpack은 일단 전부 번들부터 만드는 철학이에요. webpack serve 치면 이런 일이 벌어져요:

┌─────────────────────────────────────────────────────────────┐
│                    Webpack Dev Server                        │
├─────────────────────────────────────────────────────────────┤
│  1. 의존성 그래프 전체 파싱                                   │
│  2. 파일 전부 변환 (Babel, TypeScript 등)                     │
│  3. 전부 메모리에 번들링                                      │
│  4. 번들된 JS를 브라우저에 전달                               │
│  5. 코드 바뀌면: 영향받는 청크 다시 빌드 + HMR                 │
└─────────────────────────────────────────────────────────────┘
           │
           ▼
    ┌──────────────┐
    │   브라우저    │
    │ ────────────│
    │ <script src= │
    │ "bundle.js"/>│
    └──────────────┘

핵심은 이거예요: Webpack은 브라우저한테 뭘 주기 전에 앱 전체를 처리해야 해요. 그래서 코드베이스 커질수록 시작 시간이 비례해서 늘어나는 거죠.

Vite: "필요할 때 그때그때" 방식

Vite는 네이티브 ES 모듈을 활용해서 완전히 다른 방식으로 접근해요:

┌─────────────────────────────────────────────────────────────┐
│                    Vite Dev Server                           │
├─────────────────────────────────────────────────────────────┤
│  1. 의존성 사전 번들링 (esbuild, 일회성)                      │
│  2. index.html 즉시 제공                                     │
│  3. 브라우저 요청 시 온디맨드로 파일 변환                      │
│  4. 네이티브 ESM 사용 - 브라우저가 모듈 해석 처리              │
│  5. 변경 시: 단일 모듈 무효화, 즉각적인 HMR                   │
└─────────────────────────────────────────────────────────────┘
           │
           ▼
    ┌──────────────────────────────────────┐
    │            브라우저                   │
    │ ──────────────────────────────────    │
    │ <script type="module" src="main.js"/>│
    │                                       │
    │ import { App } from './App.tsx'      │
    │ import { useState } from 'react'     │
    │ // 브라우저가 각 모듈을 가져옴         │
    └──────────────────────────────────────┘

비결은 간단해요: Vite는 개발할 때 소스 코드를 번들링 안 해요. 대신:

  1. 의존성만 esbuild로 딱 한 번 사전 번들링 (Go로 짜여져서 JS 번들러보다 10-100배 빠름)
  2. 소스 코드는 네이티브 ES 모듈 그대로 브라우저가 달라고 할 때만 변환
  3. 실제로 열어보는 파일만 처리, 전체 코드베이스 다 안 건드림

esbuild가 왜 이렇게 빠른가

Vite 속도의 비밀 무기가 esbuild예요. 의존성 사전 번들링을 담당하는데, 벤치마크 보면 입이 벌어져요:

three.js 10개 사본 번들링 (총: ~500만 줄의 코드)

┌─────────────┬────────────────┬──────────────────┐
│   번들러    │     시간       │    상대 속도      │
├─────────────┼────────────────┼──────────────────┤
│ esbuild     │ 0.37초         │ 1x               │
│ parcel 2    │ 36.68초        │ 99배 느림        │
│ rollup      │ 38.11초        │ 103배 느림       │
│ webpack 5   │ 42.91초        │ 116배 느림       │
└─────────────┴────────────────┴──────────────────┘

esbuild가 이렇게 빠른 이유:

  • Go로 작성: 동시성 처리 끝내주는 컴파일 언어
  • 병렬 처리: 멀티코어 CPU 풀로 활용
  • AST 최소화: 진짜 필요한 변환만 함
  • 전부 메모리: 디스크 안 건드림

근데 Webpack이 아직 나은 경우도 있어요

다 Vite로 갈아타기 전에, 솔직히 Webpack이 더 나은 상황도 있어요.

1. 복잡한 커스텀 로더 요구사항

Webpack의 로더 생태계는 매우 성숙해요. 다음과 같은 커스텀 로더가 있다면:

  • 특수한 이미지 처리 파이프라인
  • 커스텀 파일 형식 변환
  • 복잡한 CSS 추출 시나리오
  • 레거시 코드 변환

Vite의 플러그인 생태계에 직접적인 대안이 없거나, 로더를 Rollup 플러그인으로 다시 작성해야 할 수도 있어요.

// Webpack - .xyz 파일용 커스텀 로더 module.exports = { module: { rules: [ { test: /\.xyz$/, use: [ { loader: 'custom-xyz-loader' }, { loader: 'xyz-preprocessor', options: { /* ... */ } } ] } ] } };

2. Module Federation (마이크로 프론트엔드)

Webpack의 Module Federation은 여전히 마이크로 프론트엔드 아키텍처를 위한 가장 성숙한 솔루션이에요:

// Webpack Module Federation new ModuleFederationPlugin({ name: 'app1', remotes: { app2: 'app2@http://localhost:3002/remoteEntry.js', }, shared: { react: { singleton: true }, 'react-dom': { singleton: true }, }, });

Vite에도 vite-plugin-federation 같은 플러그인이 있지만, Webpack의 구현이 대규모 프로덕션에서 더 많이 검증되었어요.

3. 비표준 JavaScript 환경

최신 브라우저 이외의 환경을 타겟팅하는 경우:

  • 특정 요구사항이 있는 Node.js 번들
  • 복잡한 메인/렌더러 프로세스 빌드가 있는 Electron 애플리케이션
  • 특수한 번들링이 필요한 Web Workers

Webpack의 출력 설정 유연성이 더 작업하기 쉬운 경우가 많아요.

4. 기존 Webpack 전문성을 가진 대규모 팀

팀이 수년간의 Webpack 전문성과 복잡하지만 잘 작동하는 빌드 설정을 가지고 있다면, 마이그레이션 비용이 이점을 초과할 수 있어요. 특히 현재 빌드가 "충분히 빠른" 경우에는요.

완벽한 Vite 마이그레이션 가이드

마이그레이션할 준비가 되셨나요? Create React App(Webpack 기반) 프로젝트에서 Vite로 실제 마이그레이션을 함께 살펴봅시다.

마이그레이션 전 체크리스트

시작하기 전에 현재 설정을 점검하세요:

# 현재 의존성 확인 cat package.json | grep -E "(webpack|babel|loader|plugin)" # 모든 webpack 관련 설정 파일 나열 find . -name "webpack*" -o -name ".babelrc*" -o -name "babel.config*" # 커스텀 로더/플러그인 식별 grep -r "loader:" webpack.config.js

문서화할 항목:

  • 커스텀 로더와 그 목적
  • 환경 변수 사용 패턴
  • 정적 자산 처리 요구사항
  • 프록시 설정 필요성
  • PostCSS/Tailwind 설정

1단계: Vite와 의존성 설치

# CRA 관련 패키지 제거 npm uninstall react-scripts # Vite와 관련 패키지 설치 npm install -D vite @vitejs/plugin-react # TypeScript 사용 시 npm install -D @types/node

2단계: Vite 설정 생성

프로젝트 루트에 vite.config.ts 생성:

import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import path from 'path' export default defineConfig({ plugins: [react()], resolve: { alias: { '@': path.resolve(__dirname, './src'), '@components': path.resolve(__dirname, './src/components'), '@utils': path.resolve(__dirname, './src/utils'), }, }, server: { port: 3000, open: true, // API 요청 프록시 (CRA에서 이 설정이 있었다면) proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, }, }, }, build: { outDir: 'build', sourcemap: true, rollupOptions: { output: { manualChunks: { vendor: ['react', 'react-dom'], // 필요에 따라 다른 청킹 추가 }, }, }, }, // 환경 변수 접두사 (CRA는 REACT_APP_ 사용) envPrefix: 'VITE_', })

3단계: index.html 이동 및 업데이트

Vite는 index.html이 프로젝트 루트에 있어야 해요, /public이 아니라:

mv public/index.html ./index.html

HTML 파일 업데이트:

<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/svg+xml" href="/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>My App</title> </head> <body> <div id="root"></div> <!-- 핵심 변경: Vite는 ES 모듈 사용 --> <script type="module" src="/src/main.tsx"></script> </body> </html>

주요 차이점:

  • %PUBLIC_URL%은 더 이상 필요 없음 - 절대 경로 사용
  • script 태그에 type="module" 추가
  • script가 엔트리 파일을 직접 가리킴

4단계: 엔트리 포인트 이름 변경

CRA는 src/index.tsx를 사용하지만, Vite는 관례적으로 src/main.tsx를 사용해요:

mv src/index.tsx src/main.tsx # 또는 vite.config.ts에서 index.tsx 사용하도록 설정: # build: { rollupOptions: { input: 'src/index.tsx' } }

5단계: 환경 변수 업데이트

이게 가장 큰 함정 중 하나예요. CRA는 REACT_APP_ 접두사를 사용하고, Vite는 VITE_를 사용해요:

# 모든 환경 변수 사용 찾기 grep -r "process.env.REACT_APP_" src/

옵션 A: 모든 사용 업데이트 (권장):

// Before (CRA) const apiUrl = process.env.REACT_APP_API_URL; // After (Vite) const apiUrl = import.meta.env.VITE_API_URL;

옵션 B: 호환성 심 생성 (임시 솔루션):

// src/env.ts export const env = { API_URL: import.meta.env.VITE_API_URL, DEBUG: import.meta.env.VITE_DEBUG === 'true', // ... 모든 환경 변수 매핑 };

TypeScript의 경우, 타입 정의 추가:

// src/vite-env.d.ts /// <reference types="vite/client" /> interface ImportMetaEnv { readonly VITE_API_URL: string readonly VITE_DEBUG: string // 다른 환경 변수 추가 } interface ImportMeta { readonly env: ImportMetaEnv }

6단계: 정적 자산 처리

정적 자산 처리 이동:

// Before (CRA) - Webpack이 import 처리 import logo from './logo.png'; // After (Vite) - 같은 문법, 다른 처리 import logo from './logo.png'; // URL 문자열 반환 // SVG를 React 컴포넌트로 사용하려면 플러그인 설치: // npm install -D vite-plugin-svgr import { ReactComponent as Logo } from './logo.svg';

SVGR을 위해 vite.config.ts 업데이트:

import svgr from 'vite-plugin-svgr' export default defineConfig({ plugins: [ react(), svgr({ svgrOptions: { // SVGR 옵션 }, }), ], })

7단계: CSS 모듈과 전처리기 처리

Vite는 CSS 모듈을 기본 지원해요:

// 설정 없이 바로 작동 import styles from './Button.module.css'; // SCSS의 경우, 전처리기 설치: // npm install -D sass import styles from './Button.module.scss';

PostCSS/Tailwind의 경우, Vite가 postcss.config.js를 자동 감지해요:

// postcss.config.js (이전과 동일) module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }

8단계: 패키지 스크립트 업데이트

{ "scripts": { "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", "lint": "eslint src --ext ts,tsx" } }

9단계: 일반적인 마이그레이션 이슈 처리

이슈 1: require() 사용

Vite는 ES 모듈을 사용하므로 require()가 작동하지 않아요:

// Before (CommonJS) const config = require('./config.json'); // After (ES Modules) import config from './config.json'; // 동적 require의 경우 const module = await import(`./modules/${name}.ts`);

이슈 2: 전역 변수

// Before (CRA가 제공) if (process.env.NODE_ENV === 'development') { /* ... */ } // After (Vite 대안) if (import.meta.env.DEV) { /* ... */ } if (import.meta.env.PROD) { /* ... */ } if (import.meta.env.MODE === 'development') { /* ... */ }

이슈 3: Jest에서 Vitest로 마이그레이션

Jest를 사용 중이라면 Vitest로 마이그레이션 고려 (같은 API, 더 빠름):

npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
// vitest.config.ts import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], test: { globals: true, environment: 'jsdom', setupFiles: './src/test/setup.ts', }, })
// src/test/setup.ts import '@testing-library/jest-dom';

테스트는 최소한의 변경으로 작동해요:

// Jest와 Vitest 모두에서 동일하게 작동 import { render, screen } from '@testing-library/react'; import { Button } from './Button'; describe('Button', () => { it('renders correctly', () => { render(<Button>Click me</Button>); expect(screen.getByRole('button')).toHaveTextContent('Click me'); }); });

10단계: CI/CD 업데이트

CI 설정 업데이트:

# GitHub Actions 예시 - name: Build run: | npm ci npm run build # 빌드 출력은 이제 'build/'에 있음 (vite.config.ts에서 설정 가능) - name: Deploy uses: actions/upload-artifact@v4 with: name: build path: build/

Vite 고급 최적화 기법

마이그레이션을 완료했다면, Vite에서 더 많은 성능을 끌어내는 방법을 알아볼게요.

1. 의존성 사전 번들링 최적화

어떤 의존성이 사전 번들링되는지 제어:

export default defineConfig({ optimizeDeps: { include: [ 'react', 'react-dom', // Vite가 놓칠 수 있는 의존성 포함 'lodash-es', 'axios', ], exclude: [ // ESM으로 유지해야 하는 의존성 제외 '@vueuse/core', ], }, })

2. 청크 분할 전략

프로덕션 번들 최적화:

export default defineConfig({ build: { rollupOptions: { output: { manualChunks: (id) => { // 모든 node_modules를 vendor 청크에 if (id.includes('node_modules')) { // 큰 라이브러리 추가 분할 if (id.includes('lodash')) return 'vendor-lodash'; if (id.includes('moment')) return 'vendor-moment'; if (id.includes('chart.js')) return 'vendor-charts'; return 'vendor'; } // 코드 스플리팅을 위해 기능별 분할 if (id.includes('/features/dashboard/')) return 'feature-dashboard'; if (id.includes('/features/admin/')) return 'feature-admin'; }, }, }, }, })

3. Vite 빌드 분석 활용

번들 분석:

# rollup 플러그인 설치 npm install -D rollup-plugin-visualizer
import { visualizer } from 'rollup-plugin-visualizer'; export default defineConfig({ plugins: [ react(), visualizer({ template: 'treemap', // 또는 'sunburst', 'network' open: true, gzipSize: true, brotliSize: true, filename: 'bundle-analysis.html', }), ], })

4. 환경별 설정

import { defineConfig, loadEnv } from 'vite'; export default defineConfig(({ command, mode }) => { const env = loadEnv(mode, process.cwd(), ''); return { plugins: [react()], define: { __APP_VERSION__: JSON.stringify(process.env.npm_package_version), }, build: { sourcemap: mode === 'staging', minify: mode === 'production' ? 'terser' : false, }, server: { proxy: mode === 'development' ? { '/api': env.VITE_API_PROXY_TARGET, } : undefined, }, }; });

성능 비교: 실제 벤치마크

200개 컴포넌트 React 애플리케이션의 실제 마이그레이션에서 나온 구체적인 수치를 살펴볼게요:

개발 경험

지표Webpack 5Vite 5개선
콜드 스타트 (개발)34.2초0.8초42배 빠름
웜 스타트 (캐시됨)12.1초0.3초40배 빠름
HMR (컴포넌트 변경)2.8초0.05초56배 빠름
HMR (CSS 변경)1.2초0.02초60배 빠름
메모리 사용량 (개발)1.8GB0.4GB4.5배 적음

프로덕션 빌드

지표Webpack 5Vite 5 (Rollup)개선
빌드 시간142초38초3.7배 빠름
출력 크기 (gzip)412KB398KB3% 작음
트리 쉐이킹좋음우수함더 나은 DCE

번들 품질

지표Webpack 5Vite 5
코드 스플리팅수동자동
트리 쉐이킹좋음우수함
ES 모듈 출력선택적기본값
레거시 브라우저 지원포함됨플러그인 통해

일반적인 이슈 트러블슈팅

이슈: 특정 패키지에서 "Pre-transform error"

일부 패키지는 ESM 호환이 안 돼요:

export default defineConfig({ optimizeDeps: { include: ['problematic-package'], }, build: { commonjsOptions: { include: [/problematic-package/, /node_modules/], }, }, })

이슈: CSS/LESS/SASS import 순서 문제

export default defineConfig({ css: { preprocessorOptions: { scss: { additionalData: `@import "@/styles/variables.scss";`, }, }, }, })

이슈: 동적 import가 작동하지 않음

// Before (실패할 수 있음) const Component = lazy(() => import(`./pages/${page}`)); // After (명시적 경로가 Vite의 정적 분석에 도움) const Component = lazy(() => { switch(page) { case 'home': return import('./pages/Home'); case 'about': return import('./pages/About'); default: return import('./pages/NotFound'); } });

이슈: Node.js 내장 폴리필

Vite는 기본적으로 Node.js 폴리필을 포함하지 않아요:

npm install -D vite-plugin-node-polyfills
import { nodePolyfills } from 'vite-plugin-node-polyfills'; export default defineConfig({ plugins: [ react(), nodePolyfills({ include: ['buffer', 'process'], }), ], })

미래: 2026년 이후 무엇이 올까?

빌드 도구 환경은 계속 진화하고 있어요. 주목해야 할 것들:

Rolldown: Rust 기반 Rollup

Vite 팀이 Rolldown을 개발 중이에요. Rollup의 Rust 포트죠. 예상 이점:

  • 10-20배 빠른 프로덕션 빌드
  • 완전한 Rollup 플러그인 호환성
  • Vite의 기본 번들러가 될 예정

Turbopack: Vercel의 해답

Vercel의 Rust 기반 번들러 Turbopack이 성숙해지고 있어요:

  • 네이티브 Next.js 통합
  • Webpack API 호환성 레이어
  • Next.js 환경에서 Vite 대안이 될 수 있음

Oxc: 산화 컴파일러

Rust 기반 JavaScript 툴체인:

  • 파서 (SWC보다 30배 빠름)
  • 린터 (ESLint보다 50-100배 빠름)
  • 트랜스포머
  • 미니파이어

결론: 어떻게 선택할까요?

이제 질문은 "Vite vs. Webpack" 이 아니에요. "언제 Vite로 갈아탈까?" 가 맞는 질문이죠.

Vite로 가세요:

  • 개발 서버 시작에 5초 넘게 걸림
  • HMR에 1초 이상 걸림
  • 새 프로젝트 시작함
  • 느린 빌드 때문에 팀 생산성이 떨어짐
  • 프로덕션에서 모던 브라우저만 지원해도 됨

Webpack 쓰세요:

  • 잘 돌아가는 커스텀 로더가 복잡하게 얽혀있음
  • Module Federation 많이 씀
  • 지금 빌드가 "충분히 빠름"
  • 마이그레이션 비용 > 생산성 이득
  • 특수한 출력 요구사항 있음

2026년 기준, 대부분 팀한테 새 프로젝트는 Vite가 정답이고, 기존 프로젝트도 DX 문제 있으면 마이그레이션 할 가치 있어요.

숫자가 말해줘요. 30초 시작을 300ms로 바꾸면 개발 방식 자체가 바뀝니다. 플로우에 필요한 즉각적인 피드백 루프가 생기고, 하루에 몇 시간씩 까먹던 게 사라져요.

DX가 곧 제품 속도인 시대에, 이건 있으면 좋은 게 아니라 경쟁력입니다.


빠른 참조: 마이그레이션 체크리스트

  • 현재 Webpack 설정과 커스텀 로더 점검
  • Vite와 @vitejs/plugin-react 설치
  • 동등한 설정으로 vite.config.ts 생성
  • index.html을 프로젝트 루트로 이동
  • script 태그를 type="module"로 업데이트
  • 엔트리 포인트 이름 변경 또는 Vite에서 설정
  • 환경 변수 업데이트 (REACT_APP_ → VITE_)
  • 정적 자산과 SVG import 처리
  • 필요시 CSS/SCSS 설정 업데이트
  • Jest에서 Vitest로 테스트 마이그레이션 (선택)
  • CI/CD 스크립트 업데이트
  • 프로덕션 빌드 실행 및 출력 확인
  • 벤치마크: 전/후 지표 비교
vitewebpackjavascriptbundlerperformancebuild-toolsfrontendmigration

관련 도구 둘러보기

Pockit의 무료 개발자 도구를 사용해 보세요