hongsoohyuk
HomeGuestbookProjectInstagramBlogAI Chat

© 2026 hongsoohyuk. All rights reserved.

GitHubLinkedIn
Blog/Webpack 4 → Vite 마이그레이션, 감이 아닌 근거로 설명하기

Webpack 4 → Vite 마이그레이션, 감이 아닌 근거로 설명하기

FrontendSoftware Architecture

Last edited: March 16, 2026

개요

32만줄 규모의 레거시 React 프로젝트에서 로컬 개발 서버 시작에 4~5분, HMR이 사실상 작동하지 않는 문제를 겪었다. Vite로 번들러를 교체하니 체감 속도가 극적으로 개선됐는데, "왜 빨라지는가"를 감이 아닌 구조적 근거로 설명할 수 있어야 했다. 이 글은 그 분석 과정을 정리한 것이다.

배경

대상 프로젝트의 스펙은 다음과 같았다.

  • 번들러: Webpack 4.44 (react-scripts 4.0.3 + react-app-rewired)
  • 규모: 835개 소스 파일, 317,971줄
  • node_modules: 976MB, 1,463개 패키지
  • 주요 스택: React 17, Redux + Recoil + React Query, styled-components, MUI

2020년 말에 시작된 프로젝트인데, 당시 이미 Webpack 5가 정식 릴리즈(2020년 10월)되었고 Vite 1.0도 나온 상태였다. 프로젝트 시작 시점에 이미 더 나은 선택지가 있었지만, CRA 4 + Webpack 4로 시작된 것이다.

문제

1. Cold Start 4~5분

Webpack 4는 Bundle-based Dev Server다. 개발 서버를 띄우려면 전체 소스코드를 파싱하고, 의존성 그래프를 구축하고, 번들을 생성한 뒤에야 브라우저에서 접근할 수 있다. 32만줄 + 976MB node_modules를 매번 처리하니 느릴 수밖에 없다.

추가로 발견한 번들 비효율:

문제영향
moment.js 137개 locale 전부 포함IgnorePlugin 미설정, 불필요한 5.2MB
lodash 전체 importtree-shaking 플러그인 없음, 4.9MB
core-js 풀 폴리필현대 브라우저 타겟인데 14MB 폴리필
4중 폴리필 중복core-js + regenerator + react-app-polyfill + event-source-polyfill

2. HMR 사실상 미작동

react-refresh-webpack-plugin v0.4.3은 Webpack 4에서 알려진 호환성 이슈가 있고, react-app-rewired로 CRA 내부 설정을 monkey-patch하면서 HMR 클라이언트 주입 타이밍이 꼬질 수 있다. 게다가 단일 컴포넌트에 useState가 29개인 파일에서는 React Fast Refresh가 state 보존을 포기하고 full remount를 수행한다 — 새로고침과 다를 바 없다.

3. 코드 구조가 번들러 병목을 악화시킴

번들러만의 문제가 아니었다. 코드 자체가 번들러에게 최악의 입력을 제공하고 있었다.

  • 196개 파일이 500줄 초과 — 파일 하나가 거대하면 HMR 시 리빌드 범위도 커진다
  • 5,236줄짜리 컴포넌트 — useState 29개, useEffect 10개, 핸들러 52개가 한 파일에 공존
  • 상태 관리 3중 사용 — Redux + Recoil + React Query가 동시에 존재하여 의존성 그래프가 불필요하게 복잡
  • 109개 함수를 export하는 유틸리티 파일 — 하나라도 수정하면 이를 import하는 모든 모듈이 리빌드 대상

해결: Vite가 효과적인 구조적 근거

"Vite 넣으니 빨라졌다"가 아니라, 왜 이 프로젝트에서 특히 효과적인지 설명할 수 있어야 했다.

아키텍처 차이: Bundle vs Native ESM

javascript
[Webpack 4 — Bundle-based]
시작 → 전체 파일 파싱 → 의존성 그래프 구축 → 번들 생성 → Dev Server 시작
⏱️ 32만줄 전부 처리 후에야 브라우저 접근 가능

[Vite — Native ESM]
시작 → esbuild로 deps만 pre-bundle → Dev Server 즉시 시작
→ 브라우저가 요청하는 모듈만 on-demand 변환
⏱️ 요청된 파일만 처리

이 프로젝트에서 차이가 극적인 이유:

  • 835개 파일 중 한 페이지에 필요한 건 20~30개 — Vite는 나머지를 건드리지 않는다
  • 1,463개 패키지 resolve — esbuild는 Go 네이티브라 Webpack의 JS 기반 resolve보다 10~100배 빠르다
  • moment.js 137 locale — ESM에서는 import한 locale만 로드된다

HMR 차이

javascript
[Webpack 4] 파일 변경 → 관련 청크 전체 리빌드 → 번들 교체
[Vite]      파일 변경 → 해당 모듈 1개만 ESM 교체 → 브라우저가 해당 모듈만 re-fetch

Vite의 HMR은 프로젝트 크기와 무관하게 일정한 속도를 유지한다. 5천줄짜리 파일을 수정해도 해당 모듈만 교체하면 되기 때문이다.

부수적 이점

  • react-app-rewired + customize-cra의 monkey-patch가 vite.config.js 하나로 대체된다
  • Vite의 ESM 타겟에서는 대부분의 폴리필이 불필요해진다
  • config-overrides.js에서 빠져 있던 moment locale 제거, lodash tree-shaking 같은 최적화가 Vite 생태계에선 더 자연스럽게 적용된다

배운 점

  • 번들러 교체의 근거는 수치로 만들어야 한다. "빨라졌다"가 아니라 "왜 빨라질 수밖에 없는 구조인가"를 설명할 수 있어야 한다. Bundle-based vs Native ESM의 아키텍처 차이, 프로젝트 규모별 영향도, 의존성 resolve 방식의 차이가 핵심이다.
  • 번들러 병목은 번들러만의 문제가 아닐 수 있다. 5천줄짜리 God 컴포넌트, 3중 상태 관리, 109개 함수를 export하는 유틸리티 파일 — 이런 코드 구조가 번들러에게 최악의 입력을 제공한다. Vite가 병목을 완화해주지만 근본 원인은 코드 아키텍처에 있다.
  • 레거시 프로젝트 분석 시 정량적 지표를 먼저 뽑아야 한다. 파일 수, 줄 수, useState/useEffect 개수, import 수, 중복 핸들러 수 같은 정량 데이터가 있어야 "느낌"이 아닌 "진단"이 된다.

참고 자료

  • Vite - Why Vite — Bundle vs Native ESM 아키텍처 비교
  • esbuild - FAQ — esbuild가 빠른 이유
  • React Fast Refresh — Fast Refresh의 한계와 동작 방식