Webpack Fast Refresh 대 Vite

발행: (2025년 12월 19일 오전 04:54 GMT+9)
19 min read
원문: Dev.to

Source: Dev.to

Webpack Fast Refresh vs Vite

소개

프론트엔드 개발에서 핫 모듈 교체(HMR) 는 개발 생산성을 크게 높여줍니다.
React 프로젝트에서는 Fast Refresh 라는 이름으로 HMR이 구현되어, 상태를 유지하면서 UI를 즉시 업데이트할 수 있습니다.

Webpack과 Vite 모두 Fast Refresh를 지원하지만, 내부 구현 방식과 성능에 차이가 있습니다. 이 글에서는 두 도구의 Fast Refresh 구현을 비교하고, 실제 프로젝트에 적용할 때 어떤 선택이 더 적합한지 살펴보겠습니다.

Fast Refresh란?

Fast Refresh는 React 컴포넌트가 수정될 때 전체 페이지를 새로 고치지 않고, 변경된 모듈만 교체하는 메커니즘입니다. 주요 목표는 다음과 같습니다.

  • 상태 보존: 기존 컴포넌트 인스턴스와 상태를 유지
  • 빠른 피드백: 코드 변경 후 즉시 UI에 반영
  • 오류 경계: 런타임 오류가 발생하면 자동으로 복구

Webpack에서의 Fast Refresh

설정

Webpack에서는 react-refresh-webpack-plugin@pmmmwh/react-refresh-webpack-plugin 을 사용합니다.

// webpack.config.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

module.exports = {
  mode: 'development',
  devServer: {
    hot: true,
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx|ts|tsx)$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              plugins: [require.resolve('react-refresh/babel')],
            },
          },
        ],
        exclude: /node_modules/,
      },
    ],
  },
  plugins: [new ReactRefreshWebpackPlugin()],
};

동작 원리

  1. Babel 플러그인(react-refresh/babel)이 트랜스파일 단계에서 React 컴포넌트를 감싸는 HMR 코드를 삽입합니다.
  2. Webpack Dev Server가 변경된 모듈을 감지하고, module.hot.accept 를 통해 해당 모듈만 교체합니다.
  3. React Refresh Runtime이 기존 컴포넌트 인스턴스를 재사용하면서 새로운 코드를 적용합니다.

장점

  • 기존 Webpack 생태계와 완벽히 호환
  • 커스텀 로더/플러그인과 조합이 자유로움
  • 복잡한 설정이 필요한 대규모 프로젝트에 적합

단점

  • 초기 번들링 시간이 길고, HMR 처리 로직이 복잡해 빌드 속도가 느려질 수 있음
  • 설정 파일이 많아 구성 관리가 부담스러울 수 있음

Vite에서의 Fast Refresh

설정

Vite는 기본적으로 React Fast Refresh를 내장하고 있어 별도의 플러그인 설치가 필요 없습니다. vite-plugin-react 를 사용하면 자동으로 활성화됩니다.

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()], // Fast Refresh 자동 적용
});

동작 원리

  1. Vite는 ESM 기반 개발 서버를 제공하므로, 파일이 변경되면 해당 모듈만 다시 로드합니다.
  2. @vitejs/plugin-react 내부에서 React Refresh Babel 플러그인을 적용하고, Vite의 HMR API와 연결합니다.
  3. 런타임에서 React Refresh Runtime이 기존 상태를 보존하면서 새로운 컴포넌트를 교체합니다.

장점

  • 설정이 간단하고, 별도 플러그인 설치가 거의 필요 없음
  • 번들링 단계가 거의 없기에 따라 파일 변경 시 즉시 반영되는 속도가 매우 빠름
  • 개발 서버가 가볍고, Cold Start가 빠름

단점

  • Webpack에 비해 플러그인 생태계가 아직 제한적
  • 복잡한 커스텀 로더/빌드 파이프라인을 구성하기엔 제약이 있을 수 있음

성능 비교

항목Webpack (Fast Refresh)Vite (Fast Refresh)
초기 번들링 시간5~10초 (프로젝트 규모에 따라 다름)< 1초 (ESM 기반)
파일 변경 → UI 반영300~800ms (HMR 처리 로직에 따라 차이)50~150ms (네이티브 ESM)
메모리 사용량상대적으로 높음 (번들 캐시)낮음 (가벼운 dev server)
설정 복잡도중~고 (플러그인·로더 구성 필요)낮음 (플러그인 하나만)

Note: 위 수치는 일반적인 React 프로젝트(≈ 30k LOC)를 기준으로 한 예시이며, 실제 환경에 따라 차이가 날 수 있습니다.

언제 Webpack을 선택해야 할까?

  • 대규모 레거시 프로젝트에서 기존 Webpack 설정을 그대로 유지하고 싶을 때
  • 특정 로더(예: sass-loader, file-loader)와 커스텀 플러그인을 많이 사용하고 있는 경우
  • SSR(Next.js 등)이나 멀티플랫폼(Electron, React Native) 빌드와 연동이 필요한 경우

언제 Vite를 선택해야 할까?

  • 새 프로젝트를 시작하거나, 경량화된 개발 환경을 원하는 경우
  • 빠른 피드백 루프가 중요한 UI 중심 애플리케이션(예: 디자인 시스템, 프로토타입)
  • ESM 기반으로 모듈을 직접 관리하고, 복잡한 빌드 파이프라인이 필요 없는 경우

결론

Webpack과 Vite 모두 React Fast Refresh를 제공하지만, 접근 방식과 성능 특성이 다릅니다.

  • Webpack은 유연성과 확장성이 뛰어나지만, 설정과 빌드 시간이 무거울 수 있습니다.
  • Vite는 설정이 간단하고, 파일 변경 시 즉각적인 반영이 가능해 개발 경험을 크게 향상시킵니다.

프로젝트 규모, 기존 인프라, 팀의 선호도 등을 고려해 적절한 도구를 선택하면 됩니다.

Tip: 이미 Webpack을 사용 중이라면 react-refresh-webpack-plugin 으로 충분히 빠른 개발 경험을 얻을 수 있습니다. 반면, 새 프로젝트라면 Vite를 통해 초기 설정 비용을 최소화하고 개발 속도를 극대화하는 것이 좋은 선택이 될 것입니다.

Overview

이 문서는 많은 lazy route를 가진 대규모 React + TypeScript 앱 ilert‑ui의 일상적인 개발 과정에서 가장 빠르게 느껴진 방법들을 공유합니다. 우리는 먼저 Create React App (CRA)에서 현대적인 툴링으로 전환하고, 로컬 개발을 위해 Vite를 시험해 본 뒤, 최종적으로 webpack‑dev‑server + React Fast Refresh에 정착했습니다.

이 글은 처음에 ilert 블로그에 게시되었으며, 전체 버전은 여기에서 확인할 수 있습니다.

Scope: 로컬 개발에만 적용됩니다. 프로덕션 빌드는 여전히 Webpack을 사용합니다. 참고로 React 팀은 2025년 2월 14일에 CRA를 공식적으로 종료했으며, 프레임워크나 Vite, Parcel, RSBuild와 같은 현대적인 빌드 도구로 마이그레이션할 것을 권장합니다.

ilert‑ui의 정성적 현장 기록: 공식적인 벤치마크는 진행하지 않았으며, 대규모 라우트 분할 앱에서의 일상적인 경험을 바탕으로 작성되었습니다.

Helpful Terms

TermDefinition
HMR전체 페이지를 새로 고치지 않고 변경된 코드를 실행 중인 앱에 적용합니다.
Lazy route / code‑splitting라우트를 방문할 때만 해당 라우트 코드를 로드합니다.
Vendor chunk라우트 간에 캐시되는 공유 서드파티 의존성 번들입니다.
Eager pre‑bundling나중에 발생할 많은 작은 요청을 피하기 위해 공통 의존성을 미리 번들링합니다.
Dependency optimizer (Vite)베어(import) 모듈을 사전 번들링합니다; 런타임에 새로운 의존성이 발견되면 다시 실행될 수 있습니다.
Type‑aware ESLintTypeScript 타입 정보를 활용하는 ESLint – 더 정확하지만 무거운 편입니다.

CRA가 더 이상 ilert‑ui에 맞지 않는 이유

ilert‑ui는 애플리케이션이 성장함에 따라 CRA의 편리한 기본 설정을 뛰어넘게 되었습니다. 우리가 CRA에서 벗어난 주요 이유는 다음과 같습니다:

  1. 맞춤 설정 마찰 – 고급 webpack 조정(커스텀 로더, 더 정교한 split‑chunks 전략, react-refresh를 위한 Babel 설정)에는 eject하거나 패치를 적용해야 했으며, 이는 프로덕션 규모 앱에서의 반복 작업을 늦추었습니다.
  2. 거대한 의존성 범위react-scripts가 많은 전이 패키지를 끌어들였습니다. 설치 시간이 늘어나고, 명확한 이점 없이 보안 잡음이 증가했습니다.

다음 단계 목표

  • React + TS 유지.
  • 서버 시작 후 time‑to‑interactive 개선.
  • 편집 시 상태 유지 (Fast Refresh 동작) 및 HMR을 빠르게 유지.
  • 다수의 lazy 라우트를 탐색할 때 예측 가능한 첫 방문 지연 시간 유지.

Vite: 첫인상

개발 중에 Vite는 소스를 네이티브 ESM으로 제공하고 node_modules에서 베어 임포트를 esbuild를 사용해 사전 번들링합니다. 이는 일반적으로 매우 빠른 콜드 스타트와 반응성 높은 HMR을 제공합니다.

즉시 마음에 든 점

  • Cold starts – 우리 CRA 기준보다 눈에 띄게 빠릅니다.
  • Minimal config, clean DX – 합리적인 기본값과 읽기 쉬운 오류 메시지.
  • Great HMR in touched areas – 이미 방문한 라우트 내에서 편집할 때 훌륭한 경험을 제공합니다.

모델이 우리 규모와 맞지 않았던 부분

  • Methodology – ilert‑ui에서 일상적인 개발을 통해 얻은 정성적 관찰.
  • Repo shape – 수십 개의 레이지 라우트, 많은 모듈을 끌어오는 몇몇 무거운 섹션; 수백 개의 공유 파일과 기능 전반에 걸친 깊은 스토어 임포트.

눈에 띈 점

  • First‑time heavy routes – 의존성이 많은 라우트를 처음 열면 많은 ESM 요청이 발생하고 때때로 dep‑optimizer가 다시 실행됩니다. 아직 방문하지 않은 라우트를 가로질러 탐색할 때는 공유 벤더를 적극적으로 사전 번들링하는 우리의 webpack 설정보다 느리게 느껴졌습니다.
  • Typed ESLint overheadparserOptions.project 또는 projectService와 함께 타입 인식 ESLint을 개발 서버와 같은 프로세스에서 실행하면 타이핑 중 지연이 발생했습니다. linting을 프로세스 외부로 옮기면 도움이 되었지만, 우리 규모에서는 비용을 완전히 상쇄하지 못했습니다 – 타입 기반 linting에서는 예상되는 트레이드오프입니다.

TL;DR for our codebase: 라우트가 세션 내에서 한 번이라도 터치되면 Vite는 훌륭했지만, 많은 레이지 라우트를 처음 방문할 때는 예측 가능성이 떨어졌습니다.

Source:

프로덕션에서 실행하는 내용

// webpack.config.js
module.exports = {
  optimization: {
    minimize: false,
    runtimeChunk: "single",
    splitChunks: {
      chunks: "all",
      cacheGroups: {
        "react-vendor": {
          test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
          name: "react-vendor",
          chunks: "all",
          priority: 30,
        },
        "mui-vendor": {
          test: /[\\/]node_modules[\\/](@mui\\/material|@mui\\/icons-material|@mui\\/lab|@mui\\/x-date-pickers)[\\/]/,
          name: "mui-vendor",
          chunks: "all",
          priority: 25,
        },
        "mobx-vendor": {
          test: /[\\/]node_modules[\\/](mobx|mobx-react|mobx-utils)[\\/]/,
          name: "mobx-vendor",
          chunks: "all",
          priority: 24,
        },
        "utils-vendor": {
          test: /[\\/]node_modules[\\/](axios|moment|lodash\\.debounce|lodash\\.isequal)[\\/]/,
          name: "utils-vendor",
          chunks: "all",
          priority: 23,
        },
        "ui-vendor": {
          test: /[\\/]node_modules[\\/](@loadable\\/component|react-transition-group|react-window)[\\/]/,
          name: "ui-vendor",
          chunks: "all",
          priority: 22,
        },
        "charts-vendor": {
          test: /[\\/]node_modules[\\/](recharts|reactflow)[\\/]/,
          name: "charts-vendor",
          chunks: "all",
          priority: 21,
        },
        "editor-vendor": {
          test: /[\\/]node_modules[\\/](@monaco-editor\\/react|monaco-editor)[\\/]/,
          name: "editor-vendor",
          chunks: "all",
          priority: 20,
        },
        "calendar-vendor": {
          test: /[\\/]node_modules[\\/](some-calendar-lib)[\\/]/,
          name: "calendar-vendor",
          chunks: "all",
          priority: 19,
        },
        // …additional vendor groups as needed
      },
    },
  },
  // other webpack config (loaders, plugins, devServer, etc.)
};

Note: 위 스니펫은 관련 splitChunks 설정만 보여줍니다. 나머지 webpack 설정(로더, 플러그인, devServer 등)은 그대로 유지됩니다.

왜 이 설정이 우리 팀에게 엔드‑투‑엔드로 더 빠르게 느껴졌는가

  1. 벤더 사전 번들링 – React, MUI, MobX, 차트, 에디터, 캘린더 등 벤더 청크를 명시적으로 사전 번들링합니다. 최초 로드는 다소 무겁지만, 다른 라우트에 처음 방문할 때는 공유된 의존성이 이미 캐시되어 있어 더 빠릅니다. SplitChunks가 이를 예측 가능하게 해줍니다.
  2. React Fast Refresh 사용성 – 편집 시 상태가 견고하게 유지되고, 오류 복구가 신뢰할 수 있으며, 오버레이가 잘 동작합니다.
  3. 비동기 린팅 – 타입이 지정된 ESLint가 dev server 프로세스 외부에서 실행되므로, 대규모 타입 검사 중에도 HMR이 응답성을 유지합니다.

Source:

주요 내용

  • Vite는 네이티브 ESM 제공과 번개 같은 HMR 덕분에 이미 접근한 라우트에서 빠른 반복 작업에 강점이 있습니다.
  • Webpack과 eager vendor splitting은 대규모 코드베이스에서 많은 lazy 라우트를 처음 탐색할 때 보다 예측 가능한 지연 시간을 제공합니다.
  • 무거운 도구(타입‑인식 ESLint, 타입 검사)를 개발 서버와 분리하면 선택한 번들러와 관계없이 쾌적한 개발자 경험을 유지할 수 있습니다.

비슷한 규모의 React + TS 애플리케이션에 대한 마이그레이션 경로를 평가하고 있다면, 위에서 강조한 트레이드‑오프를 고려하고 팀의 워크플로와 성능 우선순위에 가장 잘 맞는 툴체인을 선택하세요.

// webpack.config.js (excerpt)
{
  // …
  splitChunks: {
    cacheGroups: {
      "calendar-vendor": {
        test: /[\/\\]node_modules[\/\\](@fullcalendar\/core|@fullcalendar\/react|@fullcalendar\/daygrid)[\/\\]/,
        name: "calendar-vendor",
        chunks: "all",
        priority: 19,
      },
      vendor: {
        test: /[\/\\]node_modules[\/\\]/,
        name: "vendor",
        chunks: "all",
        priority: 10,
      },
    },
  },
  // …
}
// vite.config.ts – Vite `optimizeDeps` includes we tried
export default defineConfig({
  optimizeDeps: {
    include: [
      "react",
      "react-dom",
      "react-router-dom",
      "@mui/material",
      "@mui/icons-material",
      "@mui/lab",
      "@mui/x-date-pickers",
      "mobx",
      "mobx-react",
      "mobx-utils",
      "axios",
      "moment",
      "lodash.debounce",
      "lodash.isequal",
      "@loadable/component",
      "react-transition-group",
      "react-window",
      "recharts",
      "reactflow",
      "@monaco-editor/react",
      "monaco-editor",
      "@fullcalendar/core",
      "@fullcalendar/react",
      "@fullcalendar/daygrid",
    ],
    // Force pre‑bundling of these dependencies
    force: true,
  },
});

Vite – Pros

  • 번개 같은 빠른 시작과 가벼운 설정.
  • 이미 접근한 라우트 내에서 뛰어난 HMR.
  • 강력한 플러그인 생태계와 최신 ESM 기본값.

Vite – Cons

  • 의존성 최적화 재실행은 다수의 지연 라우트를 처음 탐색할 때 흐름을 방해할 수 있습니다.
  • 대규모 모노레포 및 연결된 패키지에서는 신중한 설정이 필요합니다.
  • 인‑프로세스에서 실행되는 타입드 ESLint는 대형 프로젝트에서 응답성을 저하시킬 수 있으며, 외부 프로세스에서 실행하는 것이 좋습니다.

Webpack + Fast Refresh – 장점

  • 적극적인 벤더 청크를 통해 여러 라우트에서 예측 가능한 첫 방문 지연 시간.
  • 로더, 플러그인 및 출력에 대한 세밀한 제어.
  • Fast Refresh는 상태를 보존하고 성숙한 오류 오버레이를 제공한다.

Webpack + Fast Refresh – Cons

  • Vite의 콜드 스타트보다 초기 로드가 무겁다.
  • 유지해야 할 설정 범위가 더 넓다.
  • 과거의 복잡성 (현대적인 설정 패턴과 캐싱으로 완화됨).

Choose Vite if:

  • 콜드 스타트가 워크플로우를 지배할 때.
  • 모듈 그래프가 크지 않거나 많은 레이지 라우트로 분산되지 않을 때.
  • 플러그인 – 특히 타입이 지정된 ESLint – 가 가볍거나 프로세스 외부에서 실행될 때.

Choose Webpack + Fast Refresh if:

  • 앱이 벤더 사전 번들을 적극적으로 활용하고, 여러 라우트에 걸쳐 첫 방문 지연 시간을 예측 가능하게 할 때.
  • 로더/플러그인 및 빌드 출력에 대한 정밀한 제어를 원할 때.
  • Fast Refresh의 상태 보존 및 오버레이를 선호할 때.

Learn more in the iLert blog.

Back to Blog

관련 글

더 보기 »