번들러는 왜 여전히 쓰나
지난 글 끝의 질문을 그대로 이어받는다. <script type="module">이면 브라우저가 import를 알아서 따라가며 로드해 준다 — 실제로 빌드 도구 없이 배포해도 동작한다. 그런데 왜 프로젝트는 전부 Vite나 웹팩을 거칠까. "다 그렇게 하니까"로 넘어갔던 걸 뜯어보니 이유가 여럿 있었다.
첫째, 요청의 폭포. 브라우저는 a.mjs를 받아 파싱해야 그 안의 import를 발견하고, 그걸 받아야 다음 import를 발견한다. 의존이 깊으면 요청이 폭포(waterfall)처럼 순차로 이어진다. 수백 개 모듈 프로젝트라면 첫 화면까지 왕복이 수십 번 쌓인다.
둘째, 브라우저는 'react'를 모른다. import React from 'react' 같은 베어 임포트(bare import — 경로가 아닌 패키지 이름)는 Node.js의 node_modules 해석 규칙이지 웹 표준이 아니다. 브라우저에게는 상대 경로나 URL을 주거나, 이름→URL 매핑표(import maps, 2023년부터 전 브라우저 지원)를 따로 선언해야 한다.
셋째, 브라우저가 모르는 문법. 최신 자바스크립트 문법, 그리고 애초에 표준이 아닌 TypeScript, JSX는 변환 없이는 구형(때로는 최신) 브라우저에서 파싱조차 안 된다.
넷째, 배포 최적화. 압축(minify), 안 쓰는 코드 제거, 파일 병합 — 개발할 때의 코드 모양과 배포에 유리한 모양은 다르다.
번들러는 이 간극을 메우는 도구다. 의존 그래프를 그리고 → 변환하고 → 몇 개의 파일로 합쳐서 브라우저가 바로 소화할 수 있는 형태로 만든다.
트리 셰이킹: 정적 구조가 주는 이점
번들러 이야기에서 반드시 나오는 단어가 트리 셰이킹(tree shaking)이다 — 나무를 흔들어 죽은 잎을 떨어뜨리듯, 아무도 import하지 않는 export를 번들에서 제거하는 최적화다.
// utils.js — 유틸 100개가 export되어 있어도
export const formatDate = () => { /* ... */ };
export const parseQuery = () => { /* ... */ };
// ...
// app.js — 하나만 쓰면
import { formatDate } from './utils.js';
// 번들에는 formatDate만 들어간다이게 가능한 근거가 지난 글의 핵심이었다. ESM은 정적이라서 — import/export가 실행 전에 전부 드러나므로 — 번들러가 코드를 실행하지 않고도 "이 export는 아무도 안 쓴다"를 증명할 수 있다. CJS는 require가 런타임 함수 호출이라 무엇을 쓸지 실행 전엔 알 수 없고, 그래서 트리 셰이킹이 원리적으로 어렵다. 라이브러리들이 ESM 배포판을 따로 제공하는 실질적인 이유가 이것이다.
바벨과 폴리필: 같은 "호환성"인데 하는 일이 다르다
셋째 이유(모르는 문법)를 담당하는 도구가 트랜스파일러(바벨이 대표)인데, 여기 자주 섞이는 개념이 폴리필이다. 나도 한동안 "구형 브라우저 대응 = 바벨"로 뭉뚱그려 알고 있었다. 둘은 다른 문제를 푼다.
바벨은 문법(syntax)을 변환한다. 구형 엔진이 =>라는 글자를 만나면 실행 이전에 파싱 단계에서 SyntaxError로 죽는다. 그래서 코드를 미리 옛 문법으로 고쳐 쓰는 것이다.
// 개발자가 쓴 코드
const add = (a, b) => a + b;
// 바벨이 변환한 코드
var add = function (a, b) { return a + b; };폴리필은 기능(API)을 주입한다. Promise나 Array.prototype.includes가 없는 건 문법 문제가 아니라 빌트인이 없는 문제다. 바벨이 문법을 아무리 바꿔도 없는 Promise가 생기지는 않는다 — 대신 프로미스 글에서 본 그 동작을 자바스크립트로 구현한 코드를 실행 환경에 미리 채워 넣는다(core-js가 대표). 채울 수 있는 이유도 배운 것에서 나온다 — 빌트인도 결국 전역과 프로토타입에 걸린 객체와 함수라서, 없으면 만들어 걸면 된다.
구분 기준을 한 줄로 하면: 파싱에서 죽는 문제는 바벨, 실행 중에 "undefined is not a function"으로 죽는 문제는 폴리필.
그래서 지금은
도구의 형태는 계속 바뀌고 있다. Vite는 개발 중엔 번들 없이 네이티브 ESM으로 모듈을 그대로 서빙하고(요청이 올 때마다 네이티브 도구로 즉석 변환), 배포 빌드에서만 번들링한다 — "개발 경험은 표준 ESM으로, 배포는 번들로"라는 분업이다. 브라우저와 표준이 발전할수록 번들러가 메워야 할 간극은 좁아지고 있지만, 요청 폭포·베어 임포트·비표준 언어·최적화가 사라지지 않는 한 번들링 단계 자체는 남는다.
번들러는 마법 상자가 아니라 표준이 아직 못 메운 간극을 빌드 타임에 미리 메우는 도구다. 그 간극이 무엇인지 알고 나면, 도구가 바뀌어도(웹팩 → Vite → 그다음 무엇이든) 왜 존재하는지는 흔들리지 않는다.
값과 변수에서 시작해 스코프, 클로저, this, 프로토타입, 비동기를 지나 파일 사이의 경계까지, 하나의 Why를 해결하면 다음 Whu를 엮는 구조로 회고했다. 자바스크립트를 처음겪는 사람에겐 다소 어려울 수 있어도 정말 핵심이라 할 수 있는 부분은 모듀 정리했던것 같다. 질문 지도를 위에서 아래로 훑었을 때 각 화살표가 "왜"로 설명된다면, 그것이 성장하고 있다는 증거가 될 것 같다.