브라우저는 모듈을 몰랐다
2015년 ES6가 import/export 구문을 표준으로 제정했지만, 브라우저가 이를 실제로 지원하기까지는 2년이 더 걸렸습니다. Chrome 61 (2017년 9월), Safari 11 (2017년 9월), Firefox 60 (2018년 5월) 이 <script type="module">을 지원하기 시작했습니다.
그 이전에는 브라우저에서 JavaScript 파일 간의 의존 관계를 해결할 방법이 없었습니다.
<!-- 2015년 이전: 순서에 의존하는 스크립트 로딩 -->
<script src="jquery.js"></script>
<script src="utils.js"></script>
<script src="app.js"></script>
<!-- utils.js가 jquery.js 뒤에 와야 하고, app.js가 둘 다 뒤에 와야 한다 -->파일이 10개, 50개, 100개로 늘어나면 순서를 관리하는 것만으로도 고통이었습니다. 전역 네임스페이스 오염은 덤이었습니다.
번들러의 등장
번들러 (bundler) 는 여러 JavaScript 파일을 하나 (또는 몇 개) 의 파일로 합쳐주는 도구입니다. 단순히 파일을 이어붙이는 것이 아니라, import/require 문을 분석하여 의존성 그래프를 구축하고, 올바른 순서로 모듈을 합칩니다.
번들러가 필요한 세 가지 이유입니다.
- HTTP 요청 수 감소 , 파일마다 별도의 네트워크 요청이 필요합니다. HTTP/2가 병렬 전송을 지원하지만, 수백 개의 모듈을 개별 요청하면 여전히 성능 저하가 발생합니다
- 의존성 해결 , npm 패키지의 복잡한 의존성 트리를 브라우저가 직접 해결할 수 없습니다.
node_modules안의 CommonJS 모듈은 브라우저가 이해하지 못합니다 - 코드 변환 , TypeScript, JSX, CSS Modules 등을 브라우저가 실행할 수 있는 형태로 변환해야 합니다
번들러의 동작 과정
번들러는 네 단계로 동작합니다. 엔트리 포인트에서 시작하여 의존성 그래프를 구축하고, 변환을 적용한 뒤, 최적화된 청크를 출력합니다.
// 번들러의 시작점
import App from './App';
import './styles/main.css';
index.js또는main.ts가 진입점이 됩니다.1단계: 엔트리 포인트
번들러는 설정 파일에 지정된 진입점에서 시작합니다.
// webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
},
};2단계: 의존성 그래프
엔트리 파일의 import 문을 재귀적으로 따라가며, 프로젝트 전체의 의존 관계를 하나의 그래프로 구축합니다.
index.js
App.jsx
Header.jsx
utils/format.ts
Footer.jsx
utils/api.ts
utils/http.ts
styles/main.css이 그래프가 번들러의 핵심 자료구조입니다. 어떤 모듈이 사용되고, 어떤 모듈이 사용되지 않는지, 이 그래프를 통해 판단합니다.
3단계: 변환 (Transform)
각 모듈에 로더와 플러그인을 적용합니다. Webpack에서는 로더가 파일을 JavaScript로 변환하는 역할을 합니다.
// webpack.config.js, 로더 설정
module.exports = {
module: {
rules: [
{ test: /\.tsx?$/, use: 'ts-loader' }, // TypeScript → JS
{ test: /\.jsx?$/, use: 'babel-loader' }, // JSX → JS
{ test: /\.css$/, use: ['style-loader', 'css-loader'] }, // CSS → JS
],
},
};4단계: 출력 (Output)
변환된 모듈들을 최적화된 청크로 합칩니다. 파일명에 콘텐츠 해시를 포함하여 캐시를 효율적으로 관리하고, 코드 스플리팅으로 필요한 시점에 필요한 코드만 로드합니다.
dist/
main.a3f2c8.js (애플리케이션 코드)
vendor.b7e1d9.js (라이브러리 코드)
styles.c9d4e1.css (추출된 CSS)
index.htmlESM 네이티브 시대에도 번들러가 필요한가
2017년 이후 모든 모던 브라우저가 <script type="module">을 지원합니다. 그러면 번들러 없이 ESM을 직접 쓰면 되지 않을까요?
<!-- 네이티브 ESM, 번들러 없이 작동 -->
<script type="module">
import { render } from './app.js';
render();
</script>이론적으로는 가능하지만, 실전에서는 여전히 번들러가 필요합니다.
| 문제 | 설명 |
|---|---|
| npm 패키지 | 대부분의 라이브러리가 CommonJS로 배포됩니다. 브라우저는 require()를 이해하지 못합니다 |
| TypeScript / JSX | 브라우저는 .ts, .tsx 파일을 실행할 수 없습니다 |
| 성능 | 수백 개의 모듈을 개별 HTTP 요청하면 워터폴 문제가 발생합니다 |
| 최적화 | tree-shaking, 코드 스플리팅, 압축 등은 번들러가 수행합니다 |
다만 개발 환경 에서는 이야기가 다릅니다. Vite는 개발 서버에서 네이티브 ESM을 직접 활용하여, 번들링 없이 모듈을 서빙합니다. 브라우저가 import 문을 만나면 HTTP 요청으로 해당 모듈을 가져오고, Vite 개발 서버가 중간에서 변환합니다. 이 접근 방식 덕분에 프로젝트 크기와 무관하게 서버 시작이 거의 즉각적입니다.
번들러의 진화
번들러는 계속 진화해왔습니다.
| 시기 | 도구 | 특징 |
|---|---|---|
| 2011 | Browserify | Node.js의 require()를 브라우저에서 사용 가능하게 함 |
| 2012 | Webpack | 로더/플러그인 생태계, 코드 스플리팅, HMR 등 풍부한 기능 (v1.0은 2014) |
| 2015 | Rollup | ESM 기반 설계, tree-shaking 개념 도입, 라이브러리에 적합 |
| 2020 | esbuild | Go로 작성, 기존 도구 대비 10~100배 빠른 빌드 |
| 2021 | Vite | 개발 시 네이티브 ESM 활용, 프로덕션은 Rollup으로 번들링 |
| 2026 | Vite 8 | Rust 기반 Rolldown을 단일 번들러로 채택, 10~30배 빠른 프로덕션 빌드 |
다음 단계
번들러가 왜 필요한지, 어떻게 동작하는지 이해했습니다. 다음 글에서는 가장 많이 사용되는 두 번들러, Webpack과 Vite , 의 내부 동작을 비교합니다. Webpack이 느린 구조적 이유와, Vite가 이를 어떻게 해결했는지 살펴보겠습니다.