Ray Book
프론트엔드 인프라

번들러는 왜 필요한가

import를 브라우저가 이해할 수 없던 시절부터 ESM 네이티브까지, 번들러의 존재 이유를 시각화합니다

infrabundlerwebpackviteesm

브라우저는 모듈을 몰랐다

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 문을 분석하여 의존성 그래프를 구축하고, 올바른 순서로 모듈을 합칩니다.

번들러가 필요한 세 가지 이유입니다.

  1. HTTP 요청 수 감소 , 파일마다 별도의 네트워크 요청이 필요합니다. HTTP/2가 병렬 전송을 지원하지만, 수백 개의 모듈을 개별 요청하면 여전히 성능 저하가 발생합니다
  2. 의존성 해결 , npm 패키지의 복잡한 의존성 트리를 브라우저가 직접 해결할 수 없습니다. node_modules 안의 CommonJS 모듈은 브라우저가 이해하지 못합니다
  3. 코드 변환 , TypeScript, JSX, CSS Modules 등을 브라우저가 실행할 수 있는 형태로 변환해야 합니다

번들러의 동작 과정

번들러는 네 단계로 동작합니다. 엔트리 포인트에서 시작하여 의존성 그래프를 구축하고, 변환을 적용한 뒤, 최적화된 청크를 출력합니다.

1 / 4Entry Point
진입점
index.js

// 번들러의 시작점

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.html

ESM 네이티브 시대에도 번들러가 필요한가

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 개발 서버가 중간에서 변환합니다. 이 접근 방식 덕분에 프로젝트 크기와 무관하게 서버 시작이 거의 즉각적입니다.

번들러의 진화

번들러는 계속 진화해왔습니다.

시기도구특징
2011BrowserifyNode.js의 require()를 브라우저에서 사용 가능하게 함
2012Webpack로더/플러그인 생태계, 코드 스플리팅, HMR 등 풍부한 기능 (v1.0은 2014)
2015RollupESM 기반 설계, tree-shaking 개념 도입, 라이브러리에 적합
2020esbuildGo로 작성, 기존 도구 대비 10~100배 빠른 빌드
2021Vite개발 시 네이티브 ESM 활용, 프로덕션은 Rollup으로 번들링
2026Vite 8Rust 기반 Rolldown을 단일 번들러로 채택, 10~30배 빠른 프로덕션 빌드

다음 단계

번들러가 왜 필요한지, 어떻게 동작하는지 이해했습니다. 다음 글에서는 가장 많이 사용되는 두 번들러, WebpackVite , 의 내부 동작을 비교합니다. Webpack이 느린 구조적 이유와, Vite가 이를 어떻게 해결했는지 살펴보겠습니다.