Vite is faster than Webpack

CNHur HyeonBin (Max)
Reponses  01month ago( Korean Version only )

들어가며..

현재 많은 사람들이 React를 배울 때 Create React App(CRA)이나 Vite와 같은 도구를 사용해 손쉽게 프로젝트를 생성하고, SPA(Single Page Application) 기반의 애플리케이션을 개발합니다. 이러한 도구 덕분에 리액트 프로젝트를 빠르게 시작할 수 있지만, 리액트 내부에는 실제로 매우 복잡한 설정과 구조가 존재합니다. 이러한 설정을 JavaScript로 직접 구성하는 것은 생각보다 복잡하고 어려운 일입니다.

CRA와 Vite는 이러한 복잡한 환경 구성을 자동화해주는 역할을 합니다.
CRA는 2016년 Facebook에서 출시한 도구로, 리액트 프로젝트의 기본 구조와 설정을 자동으로 생성해주는 역할을 해왔습니다. 하지만 최근에는 공식적으로 더 이상 업데이트되지 않고, 현재 상태를 유지하기로 발표된 바 있습니다.

반면, Vite는 2020년에 등장한 빌드 도구로, 빠른 개발 속도를 강점으로 내세우며 많은 인기를 얻고 있습니다.

왜 이 두 도구는 항상 비교의 대상이 되는 걸까요? 

CRA와 Vite는 리액트를 브라우저에서 동작시키기 위해 가장 핵심적인 내부 구조인 트랜스파일러와 번들러가 다르기 때문에, 목적과 성능이 매우 다르기 때문에 비교선상에 자주 오르게 됩니다. 트랜스파일러와 번들러가 무엇인지 이해하기 위해선 먼저 리액트 프로젝트가 어떻게 동작하는지를 간단히 살펴볼 필요가 있습니다.
우리는 보통 프로젝트를 작성할 때 JSX나 TSX 파일을 작성하고, 이 파일들을 브라우저에서 실행되도록 구성합니다. 하지만 이 과정이 단순히 코드를 작성하고 실행하는 것만으로 이뤄지지는 않습니다.

리액트를 정상적으로 동작시키기 위해서는 두 가지 핵심 구성 요소가 필요합니다:

  • 트랜스파일러(Transpiler)

  • 번들러(Bundler)

이 두 요소가 각각 어떤 역할을 하며, CRA와 Vite가 이들을 어떻게 다루는지에 따라 개발 경험과 성능이 달라지게 됩니다.

트랜스파일링 (Transpilng)

JSX나 TSX로 작성한 코드를 브라우저에서 바로 실행할 수 있을까요? 바로 실행시킬 수 없습니다. 브라우저는 기본적으로 JSX나 TSX 문법을 이해하지 못하기 때문에, 브라우저가 이해할 수 있는 js로 변환시켜주어야하며 이러한 작업을 트랜스파일링이라고 합니다. 

번들링 (Bundling)

JSX나 TSX를 트랜스파일러를 통해 JavaScript로 변환했다면, 이 코드는 브라우저에서 바로 실행할 수 있습니다.
이 점은 매우 중요합니다 — JavaScript는 브라우저가 기본적으로 이해하고 실행할 수 있는 언어이기 때문에, 변환만 마쳤다면 바로 동작이 가능합니다.

그렇다면, 왜 번들링이 필요한 걸까?

이 질문에 대한 답은 리액트가 주로 사용하는 구조인 SPA(Single Page Application)의 특성과 관련이 있습니다.

SPA는 한 페이지 내에서 여러 화면을 전환할 수 있도록 구성된 애플리케이션을 말하며, 페이지를 새로고침하지 않고도 다양한 콘텐츠를 표시할 수 있습니다. 이렇게 하려면 사용자가 앱을 처음 로드할 때 필요한 모든 JavaScript 파일을 미리 다운로드해두는 것이 일반적입니다. 그래야 페이지 전환 시 매끄럽게 작동할 수 있기 때문입니다.

그런데 이때, 번들링이 중요한 역할을 합니다.

HTTP는 기본적으로 TCP 기반의 프로토콜이며, 데이터를 주고받을 때마다 일정한 연결/해제 비용이 발생합니다. 단순히 파일이 작다고 해서 빠른 것이 아니라, 요청이 많아지면 그만큼 통신 오버헤드가 증가합니다.

예를 들어, 친구에게 메시지 3개를 보내야 한다고 가정해보겠습니다.
그리고 메시지를 보낼 때마다 다음과 같은 시간이 든다고 해보죠:

  • 기본 전송 시간: 1초

  • 메시지 길이에 따른 전송 시간: 메시지 내용의 길이에 따라 상이

메시지를 하나씩 따로 보낸다면,

  • 1번째 메시지: 기본 1초 + 길이에 따른 시간 1초 = 2초

  • 2번째 메시지: 기본 1초 + 길이에 따른 시간 1초 = 2초

  • 3번째 메시지: 기본 1초 + 길이에 따른 시간 1초 = 2초
    총 6초

하지만 메시지 3개를 한 번에 묶어서 보낸다면,

  • 기본 1초 + 길이에 따른 시간 3초 = 총 4초

즉, 작은 데이터라도 여러 번 나눠 보내면 오히려 느릴 수 있다는 것이고, 이것이 번들링의 핵심 이유입니다.

JavaScript 파일도 마찬가지입니다. 여러 개로 나눠 전송하면 매 요청마다 오버헤드가 발생하고, 네트워크 효율이 떨어지게 됩니다. 그래서 번들러는 여러 개의 JS 파일을 하나로 묶어, 브라우저가 한 번에 받아볼 수 있도록 처리합니다. 이를 통해 로드 속도 개선, 초기 렌더링 최적화, 트래픽 절감 등의 효과를 기대할 수 있습니다.

내부 구조와 동작 방식 차이 

CRA와 Vite는 리액트 프로젝트를 실행하기 위해 서로 다른 도구들을 사용합니다.

  • CRA:

    • 트랜스파일러: Babel

    • 번들러: Webpack

  • Vite:

    • 트랜스파일러: esbuild

    • 번들러: Rollup

이 중 esbuild는 Go 언어로 작성된 고성능 빌드 도구로, 속도 면에서 Babel보다 훨씬 빠릅니다. 이로 인해 Vite는 빌드 성능에서도 큰 이점을 가집니다.

리액트 프로젝트는 크게 개발 모드배포 모드 두 가지로 나뉘며, 각 모드 별 단계는 아래와 같습니다.

CRA

  • 개발 모드:
    코드 → 트랜스파일링 → 번들링 → 브라우저 렌더링

  • 배포 모드:
    코드 → 트랜스파일링 → 번들링 → 브라우저 렌더링

Vite

  • 개발 모드:
    코드 → 트랜스파일링 → 브라우저 렌더링

  • 배포 모드:
    코드 → 트랜스파일링 → 번들링 → 브라우저 렌더링

Vite는 개발 모드에서 번들링을 생략합니다. 대신 모듈 단위로 코드를 처리하고, 필요한 시점에 필요한 모듈만 불러오는 방식(HMR 기반, HMR관련 설명은 아래에 소개됩니다.)을 사용합니다. 이는 앞서 설명한 번들링의 필요성과 함께, 개발 환경에서는 굳이 전체 번들링을 하지 않아도 되는 이유와 맞닿아 있습니다.

결과적으로 Vite는 개발 모드에서 번들링을 생략하기 때문에, CRA보다 훨씬 빠른 실행 속도를 제공합니다.
또한, 트랜스파일러 자체의 성능 차이(esbuild vs Babel)도 존재하므로, 단순한 처리 단계의 차이뿐만 아니라
초기 로딩 시간, HMR 반응 속도에서도 체감 가능한 성능 차이가 발생합니다.

esbuild vs Babel

esbuild와 Babel은 개발 목적과 내부 구조부터 다르기 때문에, 트랜스파일링 성능에서도 뚜렷한 차이를 보입니다.
CRA와 Vite가 각각 이 두 트랜스파일러를 선택한 이유도, 도구의 설계 철학과 성능 지향점의 차이에서 비롯된 것입니다.

  • Babel
    Babel은 비교적 초기에 등장한 트랜스파일러로, JavaScript로 개발되었으며 CommonJS 기반으로 구성되어 있습니다.
    구조가 유연하고 생태계가 넓다는 장점이 있지만, JS 자체의 단점인 싱글 스레드 처리, 런타임 성능 한계 등으로 인해 트랜스파일링 속도가 상대적으로 느립니다.

  • esbuild
    esbuild는 Go 언어로 작성된 고성능 트랜스파일러로, 비교적 최근에 등장했습니다.
    Go의 고속 컴파일 특성과 멀티 스레드 병렬 처리 능력을 바탕으로, Babel보다 훨씬 빠른 빌드 속도를 자랑합니다. 비교적 최근에 등장했기 때문에, 성능에 이점이 있지만, 그만큼 생태계가 Babel만큼 넓지 않다는 단점이 존재합니다.

Webpack vs Rollup

1. 내부 구조와 개발 철학 차이

Webpack과 Rollup은 번들링 작업을 처리하는 방식에서 근본적인 철학 차이를 보입니다. Webpack은 역할별 분리된 설정 체계를 통해 세분화된 제어를 제공하는 반면, Rollup은 플러그인 중심의 단일 체계로 모든 기능을 통합합니다.

Webpack의 3단계 분리 체계

로더(Loaders): 파일 변환 엔진

module: {
  rules: [
    {
      test: /\.tsx?$/,
      use: 'ts-loader',
      exclude: /node_modules/
    }
  ]
}

 

로더는 파이프라인 방식으로 동작합니다. 특정 파일 패턴을 감지하면 해당 파일을 JavaScript가 이해할 수 있는 형태로 변환합니다. TypeScript를 JavaScript로, SCSS를 CSS로, 이미지를 Base64로 변환하는 등의 작업을 담당합니다.

벤더 분리(Vendor Splitting): 캐싱 최적화 전략

optimization: {
  splitChunks: {
    cacheGroups: {
      vendor: {
        test: /[\\/]node_modules[\\/]/,
        name: 'vendor',
        chunks: 'all'
      }
    }
  }
}

 

벤더 분리는 캐시 무효화 최소화를 목표로 합니다. 자주 변경되지 않는 외부 라이브러리(React, Lodash 등)를 별도 청크로 분리하여, 애플리케이션 코드가 변경되어도 라이브러리 캐시는 유지됩니다.

플러그인(Plugins): 빌드 프로세스 확장

plugins: [
  new HtmlWebpackPlugin({
    template: './src/index.html'
  }),
  new MiniCssExtractPlugin({
    filename: '[name].[contenthash].css'
  })
]

플러그인은 빌드 생명주기의 특정 시점에 개입하여 추가 작업을 수행합니다. HTML 파일 생성, CSS 추출, 번들 분석 등 로더가 담당하지 않는 모든 부가 기능을 처리합니다.

실제로 CRA로 기본 리액트 프로젝트를 생성하고 npm run eject 명령어로 webpack.config.js를 확인해보면 이러한 3단계 구조가 명확하게 분리되어 있는 것을 볼 수 있습니다.

some thing

CRA의 webpack.config.js를 살펴보면 많은 코드가 존재하지만, exports되는 함수 내에서 loader, optimization, plugin이 명확하게 분리되어 있는 것을 확인할 수 있습니다.

이러한 구조는 관심사의 명확한 분리를 보여주며, 각 설정 영역이 독립적인 책임을 가집니다. 파일 변환은 loader가, 번들 최적화는 optimization이, 빌드 프로세스 확장은 plugin이 담당하는 방식으로 역할이 구분됩니다.

이를 통해 개발자는 복잡한 빌드 프로세스를 단계별로 세밀하게 제어할 수 있으며, 특정 기능만 수정하거나 확장할 때도 해당 영역만 집중적으로 다룰 수 있습니다.

Rollup의 플러그인 중심 아키텍처

CRA의 Webpack과 다르게 Rollup은 모든 기능을 플러그인으로 통합하여, 플러그인 중심으로 한번에 다룹니다.

export default {
  input: 'src/index.js',
  output: {
    file: 'dist/bundle.js',
    format: 'esm'
  },
  plugins: [
    // 파일 변환 (Webpack의 로더 역할)
    typescript(),
    babel({
      babelHelpers: 'bundled'
    }),
    
    // 외부 의존성 처리 (Webpack의 벤더 역할)
    nodeResolve({
      browser: true
    }),
    commonjs(),
    
    // 빌드 최적화 (Webpack의 플러그인 역할)
    terser(),
   
    serve({
      contentBase: 'dist',
      port: 3000
    })
  ]
}

 

Webpack의 장점:

  • 세밀한 제어: 각 단계별 독립적 설정이 가능하여 복잡한 요구사항에 대응
  • 확장성: 복잡한 빌드 파이프라인 구성에 유리한 모듈화된 구조

Rollup의 장점:

  • 단순성: 통일된 플러그인 인터페이스로 학습 곡선과 설정 복잡도 최소화

결국 Webpack은 복잡한 애플리케이션을, Rollup은 라이브러리나 간단한 앱을 대상으로 하는 설계 철학의 차이가 이러한 아키텍처 차이를 만들어냅니다. Webpack의 3단계 분리 체계는 대규모 프로젝트에서 요구되는 세밀한 제어를 위한 것이며, Rollup의 플러그인 중심 구조는 효율성과 단순함을 추구하는 철학을 반영합니다.

2. 번들링 결과의 모듈 시스템의 차이

Webpack과 Rollup은 모두 소스코드를 JavaScript 파일로 번들링하는 '번들링 도구'입니다. 하지만 JavaScript에는 많은 모듈 시스템이 존재하고, Webpack은 소스코드를 Runtime Module System 기반으로 번들링하며, Rollup은 ES6 모듈 시스템을 기반으로 번들링합니다.

다시 말해, 같은 소스코드라도 Webpack으로 번들링한 결과와 Rollup으로 번들링한 결과가 다릅니다.

간단한 소스코드가 존재하고, 이를 webapck과 rollup을 통해 번들링을 진행한 후 결과를 비교해 보았습니다.

//math.js
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
export function unused() { return 'unused'; }

// main.js
import { add } from './math.js';
console.log(add(2, 3));

webpack 결과 (눈으로만 한번 훑어보아도 됩니다):

/******/ (() => { // webpackBootstrap
/******/ 	"use strict";
/******/ 	var __webpack_modules__ = ({

/***/ "./math.js":
/*!*****************!*\
  !*** ./math.js ***!
  \*****************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
  "add": () => (/* binding */ add),
  "multiply": () => (/* binding */ multiply),
  "unused": () => (/* binding */ unused)
});

function add(a, b) { return a + b; }
function multiply(a, b) { return a * b; }
function unused() { return 'unused'; }

/***/ }),

/***/ "./main.js":
/*!*****************!*\
  !*** ./main.js ***!
  \*****************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
var _math_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./math.js */ "./math.js");

console.log((0,_math_js__WEBPACK_IMPORTED_MODULE_0__.add)(2, 3));

/***/ })

/******/ 	});
/************************************************************************/
/******/ 	// 런타임 코드 (모듈 로더 시스템)
/******/ 	var __webpack_require__ = {};
/******/ 	
/******/ 	__webpack_require__.d = (exports, definition) => {
/******/ 		for(var key in definition) {
/******/ 			if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ 				Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ 			}
/******/ 		}
/******/ 	};
/******/ 	
/******/ 	__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ 	
/******/ 	__webpack_require__.r = (exports) => {
/******/ 		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ 			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ 		}
/******/ 		Object.defineProperty(exports, '__esModule', { value: true });
/******/ 	};
/******/ 	
/******/ 	// 모듈 실행 시작
/******/ 	__webpack_require__("./main.js");
/******/ })();

rollup 결과:

function add(a, b) { return a + b; }
console.log(add(2, 3));

번들링 결과물의 핵심 차이점

앞서 언급했듯이 Webpack과 Rollup은 다른 모듈 시스템과 개발 철학을 가지고 있기 때문에, 같은 코드를 번들링해도 결과가 아예 다르다는 것을 확인할 수 있습니다.

Runtime Module System vs ES6 모듈 시스템의 차이

Runtime Module System은 동적 임포트와 코드 스플리팅을 가능하게 하여 필요한 모듈만 런타임에 로드할 수 있다는 장점이 있고, CommonJS 기반이기 때문에 오래된 레거시 코드와 호환성이 좋습니다. 반면 ES6 모듈 시스템은 정적 분석을 통해 빌드 타임에 불필요한 코드를 제거하여 더 작은 번들을 생성할 수 있습니다.

구체적인 결과물 차이점

코드 구조:

  • Webpack: 모듈별로 분리된 구조 유지
  • Rollup: 하나의 플래트한 코드로 병합

런타임 오버헤드:

  • Webpack: 모듈 시스템 코드 포함 (~2KB)
  • Rollup: 순수 코드만 (~50바이트) (우세)

트리 셰이킹:

  • Webpack: unused 함수가 남아있음
  • Rollup: unused, multiply 함수 완전 제거 (우세)

3. HMR 성능 차이

Hot Module Replacement(HMR)는 개발 모드에서 소스코드 변경이 감지되었을 때, 페이지 새로고침 없이 변경된 모듈만 교체하여 즉각 반영하는 기술입니다.

HMR은 개발자가 소스코드를 변경했을 때 화면에 나타나는 요소나 컴포넌트를 state 변경 없이, 새로고침 없이 즉각 반영하는 기술로서, 개발 모드 절차가 빠른 Rollup이 속도 측면에서 Webpack보다 빠르다는 장점이 있습니다.

물론 Webpack 역시 특정 파일이 변경될 때마다 프로젝트의 모든 파일을 트랜스파일링하고 번들링을 처음부터 다시 하는 것은 아닙니다. 그렇게 되면 HMR 시에 굉장히 많은 시간이 소요되고, 하드웨어적으로도 큰 부하가 가해질 것입니다. 내부적으로는 변경되지 않은 파일의 JavaScript 버전과 트랜스파일링이 완료된 부분을 캐싱해두고, 변경된 부분만 트랜스파일링을 다시 하여 번들링만 진행하게 됩니다. 또한 개발자의 재량으로 이러한 캐싱 설정을 고도화하여 최적화도 가능합니다.

하지만 아무리 캐싱 최적화가 잘된 Webpack이라 해도, 절대적인 번들링 과정이 없는 Rollup보다 HMR 속도가 빠르기는 어렵습니다.

4. 빌드타임 차이

GitHub의 JIRA Clone 프로젝트를 클론하여 Webpack과 Rollup으로 각각 번들링한 결과는 다음과 같았습니다.

측정 결과:

  • Webpack: 2.3MB (빌드 시간: 2.234초)
  • Rollup: 1.6MB (빌드 시간: 7.507초)

테스트용 project github link: 여기에 실제 링크 들어갈거임

빌드 속도는 Webpack이 월등히 빨랐지만, 최종 번들 크기는 Rollup이 더 작다는 것을 확인할 수 있었습니다.

일반적으로 Vite나 Rollup이 속도 측면에서 장점이 있다고 알려져 있지만, 이번 테스트에서는 Webpack이 3배 이상 빠른 결과를 보였습니다. 이러한 결과가 나온 주요 원인은 레거시 라이브러리 의존성(과거에 작성된 프로젝트로 CommonJS 기반 라이브러리를 다수 사용)과 대규모 프로젝트 최적화(많은 컴포넌트를 포함한 복잡한 프로젝트에서 Webpack의 캐싱 시스템이 효과적으로 작동) 때문입니다.

이는 Vite나 Rollup의 빌드 속도가 항상 CRA나 Webpack보다 빠르다고 단언하기 어렵다는 점을 나타내며, 프로젝트의 규모와 특성에 따라 빌드 성능은 달라질 수 있습니다.

번들링된 최종 파일 크기를 살펴보면 Rollup이 Webpack보다 약 30% 작은 결과를 보였으며, 이는 Runtime Module System과 ES6 모듈 시스템의 특성 차이가 제대로 반영된 결과로 볼 수 있습니다. Rollup의 플래트 번들링과 효율적인 트리 셰이킹이 더 작은 번들 생성에 기여한 것으로 분석 할 수 있을 것 같습니다.

Webpack vs Rollup 최종 결과

앞서 살펴본 비교를 보면, 속도나 파일 크기, 빌드 절차 등 성능적인 측면에서는 Webpack이 Vite보다 다소 약세를 보이는 것을 확인할 수 있습니다. 이는 Vite가 Webpack보다 비교적 최근에 개발되었고, 더 최신 기술이 반영된 도구이기 때문에 어느 정도 자연스러운 결과라고 볼 수 있습니다.

그럼에도 불구하고 Webpack은 오랜 시간 동안 꾸준히 사용되어 온 만큼, 여전히 많은 프로젝트에서 활용되고 있습니다. 이런 오랜 역사는 단순히 시간이 흘렀다는 의미를 넘어서, 수많은 테스트를 통해 안정성이 입증되었고, 방대한 플러그인 생태계로 확장성도 뛰어나다는 점에서 중요한 의미를 가집니다. 또한, 다양한 커뮤니티와 기여자들이 존재한다는 점 역시 큰 장점으로 작용합니다. 개발을 진행하면서 안정성, 유연성, 그리고 함께 문제를 논의할 수 있는 커뮤니티가 있다는 것은 매우 중요한 요소로 여겨지기에 Webpack을 바탕으로한 프로젝트 또한 꾸준히 개발되고 있다고 생각합니다.

이러한 이유로, 특히 안정성이 최우선이며 복잡한 요구사항을 자주 다뤄야 하는 금융권이나 대기업에서는 여전히 Webpack을 선호하는 경우가 많습니다. 반면, 하루에도 여러 번 배포가 이뤄지고 빠른 개발 속도가 중요한 스타트업이나, 복잡한 빌드 설정이 필요 없는 비교적 단순한 구조의 프로젝트를 진행하는 기업에서는 Vite를 사용하는 경우가 점점 늘고 있는 추세입니다.

물론, Webpack이 더 안정적이고 큰 커뮤니티를 갖고 있다고 해서 Vite가 불안정하거나 커뮤니티가 부족하다는 의미는 아니며, 각 도구는 저마다의 장점이 있고, 중요한 것은 현재 진행 중인 프로젝트의 성격과 요구사항에 맞는 빌드 도구를 선택하는 일인것 같습니다.


마치며

이번 글에서는 대표적인 두 가지 트랜스파일러와 번들러를 비교해보았습니다. 내부 동작 방식을 이해하는 것은 단순히 사용자 경험뿐 아니라, 개발자 경험을 높이는 데에도 큰 도움이 됩니다. 다양한 도구들의 특성을 알아두면, 프로젝트 상황에 맞는 도구를 더 잘 선택할 수 있게 됩니다.

지금 사용 중인 도구가 익숙하다고 해서 다른 도구를 전혀 알 필요가 없는 것은 아닙니다. 경쟁 도구에 대해서도 기본적인 이해를 갖고 있으면, 더 나은 기술적 판단을 내릴 수 있습니다. 기술 선택의 폭이 넓어진다는 점에서 충분히 가치 있는 일입니다.

이 글을 통해 빌드 도구에 대해 조금 더 친숙하게 느끼셨길 바랍니다. 자신이 평소 사용하던 도구가 어떤 역할을 해왔는지 돌아볼 수 있는 계기가 되었으면 좋겠고, 앞으로 새로운 프로젝트를 시작할 때 어떤 기준으로 도구를 선택해야 할지 판단하는 데에도 도움이 되었기를 바랍니다.

CNHur HyeonBin (Max)
Reponses  0