아직도 번들러를 써야하는 이유?
어떤 문제가 있었나?
브라우저 모듈 시스템이 아직 표준화되지 않았던 옛날에는 자바스크립트를 HTML script 태그로 연결하여 사용했고 자바스크립트 모듈화를 네이티브 레벨에서 진행할 수 없었습니다.
그리고 이러한 방식은 3가지 문제를 발생시킵니다.
파일 수만큼 네트워크 요청
의존성 관리가 어려워지는 문제
변수의 전역 오염 문제
등장한 모듈 시스템과 모듈 번들러?
모듈처럼 구현하면서도 실제 런타임환경에서 사용할 자바스크립트를 브라우저가 알아들을 수 있도록 번들링하는 형태로 시작했습니다.
여러 파일을 브라우저가 이해할 수 있는 표준(HTML, CSS, JS)으로 변환해야 합니다.

출처: Webpack
모듈 시스템
CommonJS라는 모듈 시스템 제안
export
// named exports module.exports.sum = (a, b) => a + b; const { sum } = require('./sum.cjs'); // default exports module.exports = (a, b) => a + b; const sum = require('./sum.cjs');
require
CJS가 기본값
ES6에서 ESM(ES Module) 방식 채택
export
// named exports export const sum = (a, b) => a + b; import { sum } from './sum.mjs'; // default exports export default (a, b) => a + b; import sum from './sum.mjs';
import, 동적 import는 await import
CJS가 기본값이므로 **
.js
**를 **.mjs
**로 바꾸거나, **package.json
**에"type": "module"
옵션을 넣는 방법이 있다. (기존에 CJS를 쓰던 것은 **.cjs
**로 바꾸면 된다.)(기본 설정에 고려) **
use strict
**가 설정되어 있어야하고, **this
**는 global object를 참조
CJS와 ESM 지원
CJS → ESM
기본적으로 CJS 모듈은 import하면
.default
로 감싸짐ESM 지원하는 라이브러리를 쓰거나
await import('lodash’)
를 사용CJS의 named exports를 import할 수 없다.
CJS는 named exports를 실행단계에서 연산하지만, ESM은 named exports를 파싱 단계에서 연산하기 때문
// 이 방식은 tree shaking이 안되고 순서보장도 안됩니다. import _ from './sum.mjs'; const { sum } = _;
CJS와 ESM 둘 다 지원하려면
CJS/ESM 버전으로 별도의 라이브러리 지원
CJS 라이브러리에 ESM 래핑 지원
수백 수천 개의 함수를 일일이 export 해야 하므로 대규모 라이브러리에는 비효율적일 수 있습니다.
import _ from 'lodash'; export default _;
package.json에 exports 추가
shared 패키지를 복제해서 두 버전을 위한 진입점을 물리적으로 분리하는 것
아직도 번들러를 써야하는 이유?
ESM이 표준화된 ES6 등장 이후 10년 가까이 지난 상황에서 이제 거의 모든 브라우저가 ESM을 기본적으로 지원하는데 번들러가 필요한 이유는 Vite를 사용해야하는 이유에서 찾아봤어요
어떤 번들러가 있는지?
처음에는 Webpack과 같은 도구를 이용했는데 어플리케이션이 커짐에 따라 Javascript 모듈의 갯수도 증가하며 Javascript 기반의 도구에서 성능 병목이 발생했고 HMR을 사용하더라도 느린 피드백 루프를 가지게 되면서 브라우저에서 지원하는 ESM(ES Module) 및 네이티브 언어로 작성된 자바스크립트 도구를 활용해 문제를 해결하고자 합니다.
State of FE·JS Korea 2025(https://naver.github.io/fe-news/stateof-fejs/2025/)의 결과를 보았을 때
Vite
> Turbopack
> esbuild
> Webpack
> Rollup
순으로 확인할 수 있었습니다.
Vite
어떻게 동작하는가?
개발모드에서는 esbuild
모든 코드를 네이티브 ESM으로 CommonJS 그리고 UMD 모듈을 ESM으로 가져오기
모노리포에서는 개발서버에서 안가져올시에 build.commonjsOptions, optimizeDeps에 추가해야합니다.
개발 시 unbundled ESM 사용, 종속성은 esbuild로 사전 번들링합니다.
기존 번들러들은 개발 시에도 모든 파일을 하나로 묶어서 제공했어요. 하지만 Vite는 각 파일을 개별적으로 제공해서 전체 다시 번들링 안해도되고, 변경된 모듈만 다시 로드(HMR 빠름)
모든 걸 unbundled로 하면 네트워크 요청이 많을 수 있으니 종속성은 개발모드에서 번들링
프로덕션모드에서는 rollup 사용합니다.
HTTP 헤더를 활용하여 전체 페이지의 로드 속도를 올립니다(
304 Not Modified
,Cache-Control: max-age=31546000, immutable
)
주의할 점
기본적으로 index.html이 진입점이기 때문에 라이브러리 모드 존재(https://ko.vite.dev/guide/build.html#library-mode)
ES5 이하는 별도로 polyfill
Rust로 작성되었으며 Next.js에 내장
파일 변경 → 변경된 부분만 다시 빌드하므로 속도 개선
빌드시 함수 단위로 캐싱
SWC 컴파일러 사용
Yarn PnP 미지원
GO를 이용한 병렬처리, 메모리 사용
자체 JS파서 지원으로 속도 최적화
네이티브 ESM 사용
es5 이하의 문법을 일부 미지원
HMR 미지원
Javascript 모듈 번들러
모듈 번들링
하나의 파일이 다른 파일에 의존할 때마다, webpack은 이것을 의존성으로 취급하여 엔트리 포인트에서 시작하여 모든 모듈을 포함하는 디펜던시 그래프를 재귀적으로 빌드합니다.
IIFE를 사용
모듈 맵 사용
각 모듈을 함수로 래핑합니다.
모듈을 함께 붙이는 런타임 ****코드가 있습니다.
브라우저에서 모듈을 관리하기 위해 추가 코드를 생성
development mode
개발 서버 실행(HMR)
소스맵
production mode
코드 압축
코드 스플리팅
다양한 번들로 분할
중복된 모듈이 있을 경우 N개의 번들에 모두 포함
⇒
dependOn
으로 공유할 수 있습니다.
모듈 번들러
모듈 맵이 없습니다.
모듈의 함수 래핑이 없습니다.
모듈 내에서 선언된 모든 변수/함수는 이제 전역 범위로 선언됩니다.
모든 모듈을 하나의 스코프에 호이스팅
충돌 방지를 위해 변수/함수 이름을 바꿉니다.
ES Module 형태 번들 지원(웹팩은 초기에는 미지원)
Tree Shaking 지원
단점은 순환 참조를 잘 해결해주지 않습니다.
vs webpack
webpack은 내부적으로 CommonJS, rollup은 typescript(ES6)
webpack은 CommonJS형태로 빌드(v5부터는 es6 지원), rollup은 ES6 module형태로 빌드
webpack은 모듈을 함수로 감싸서 평가, rollup은 모듈들을 호이스팅
treeshaking은 기본적으로 es6코드에서 제대로 동작하기에 rollup이 유리
Reference
Last updated