바벨은 무엇을 하고 무엇을 하지 않는가
바벨을 이해하는 가장 빠른 방법은 결과물을 열어 보는 것입니다. 한 파일을 넣고 나온 파일을 열면 무슨 일을 했는지 한눈에 보입니다. 그런데 그때 같이 알게 되는 것이 하나 더 있습니다 — 바벨은 생각보다 훨씬 적은 일만 합니다. 합치지도 않고, 없는 기능을 채워 주지도 않습니다. 이 경계를 모르면 "빌드는 됐는데 브라우저에서 안 돈다"에서 며칠을 씁니다.
원문은 2020년 3월, 바벨 7 시절에 쓴 노트입니다. 최소 설정으로 돌려 보는 순서는 지금도 그대로 쓸 수 있지만, 바벨 8 이 2026년 6월에 나왔고 설정 파일 권장값도 그 사이에 바뀌었습니다. 명령과 설정은 지금 기준으로 고쳤고, 6절에 지금 이 도구를 쓸 자리가 어디인지 적었습니다.
원문에는 @babael/core 같은 오타와 변수명이 어긋난 예제가 있었습니다.
옮기면서 실제로 돌려 보고 고쳤습니다.
1. 넣은 것과 나온 것
화살표 함수와 템플릿 리터럴을 쓴 파일 하나를 src/index.js 에 둡니다.
src/index.jsconst sayHello = (name) => {
return `Hello ${name}`
}
export default sayHello
이걸 바벨에 통과시키면 이렇게 나옵니다.
lib/index.js — 오래된 브라우저를 대상으로 했을 때"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = void 0;
var sayHello = function sayHello(name) {
return "Hello ".concat(name);
};
var _default = exports.default = sayHello;
바뀐 것을 하나씩 짚으면 바벨이 하는 일이 전부 드러납니다.
- 화살표 함수 →
function표현식 - 템플릿 리터럴 →
.concat() const→varexport→exports.default(CommonJS)"use strict"가 붙었다 — ES 모듈은 원래 엄격 모드라 그 의미를 유지한다
전부 문법을 다른 문법으로 바꾼 것입니다. 이게 바벨의 정체입니다 — 같은 뜻의 코드를 더 낮은 버전의 문법으로 다시 쓰는 도구입니다.
2. 직접 돌려 보기
mkdir sample && cd sample
npm init -y
npm install --save-dev @babel/core @babel/cli @babel/preset-env
세 개가 각각 다른 일을 합니다. @babel/core 는 변환 엔진이고,
@babel/cli 는 명령줄에서 부르는 껍데기이며,
@babel/preset-env 는 무엇을 어디까지 바꿀지를 아는 규칙
묶음입니다. 코어만 깔면 아무것도 변환되지 않습니다 — 바벨의 기본값은
"아무 일도 하지 않음"입니다.
설정 파일을 프로젝트 루트에 둡니다.
babel.config.json{
"presets": [
["@babel/preset-env", { "targets": "> 0.5%, last 2 versions, not dead" }]
]
}
package.json"scripts": {
"build": "babel src --out-dir lib"
}
npm run build
원문에서 고친 것 — 설정 파일 이름
원문은 .babelrc.json 을 썼습니다. 동작은 하지만
babel.config.json 이 맞는 선택인 경우가 대부분입니다. 둘은 적용
범위가 다릅니다.
| 파일 | 적용 범위 |
|---|---|
babel.config.json |
프로젝트 전체. node_modules 안의 파일이나 모노레포의
다른 패키지까지 닿는다 |
.babelrc / .babelrc.json |
그 파일이 있는 패키지 안에서만. 경계를 넘으면 무시된다 |
그래서 .babelrc 를 쓰면 모노레포에서 다른 워크스페이스의 코드가
변환되지 않거나, 최신 문법으로 배포된 라이브러리를 못 다루는 일이 생깁니다.
원인을 찾기 어려운 축에 듭니다 — 설정은 분명히 있는데 특정 파일에만 안 먹습니다.
3. targets 를 비워 두면 안 되는 이유
원문은 preset-env 를 옵션 없이 썼습니다. 그러면 preset-env 는 대상
브라우저를 모르므로 알고 있는 모든 변환을 켭니다. 결과는 ES5 입니다.
2020년에는 그게 안전한 기본값이었습니다. 지금은 손해입니다.
-
코드가 커집니다.
async/await를 ES5 로 낮추면 제너레이터 런타임이 통째로 따라붙어 몇십 KB 가 늘어납니다. - 느려집니다. 브라우저가 네이티브로 지원하는 문법을 굳이 흉내 낸 코드로 바꿔 실행하게 됩니다.
- 필요도 없습니다. 지금 쓰이는 브라우저는 사실상 전부 ES2020 이상을 이해합니다.
package.json 에 적어 두면 다른 도구도 같은 값을 읽는다"browserslist": [
"> 0.5%",
"last 2 versions",
"not dead"
]
browserslist 는 바벨 전용이 아닙니다. Autoprefixer 도, 번들러도
같은 값을 읽습니다. 여기 한 곳에 적어 두면 도구마다 대상이 어긋나는 일이
없어집니다. 실제로 무엇이 켜지는지는 다음 명령으로 볼 수 있습니다.
npx browserslist
4. 바벨이 하지 않는 일 ① — 폴리필
가장 자주 걸리는 오해입니다. preset-env 는 문법만 바꿉니다. 없는 기능을 만들어 주지는 않습니다.
- 화살표 함수,
class - 구조 분해, 스프레드
- 템플릿 리터럴
async/await, 옵셔널 체이닝
Promise,Map,SetArray.prototype.includesObject.entriesfetch,IntersectionObserver
구분선이 뚜렷합니다. 파서가 읽지 못하는 것은 문법이고, 읽기는 읽는데
실행할 때 없는 것은 기능입니다. [1,2].includes(1) 은 ES5 파서도
문제없이 읽습니다 — 그냥 메서드 호출이니까요. 그래서 바벨은 손댈 이유가 없고,
구형 브라우저에서 includes is not a function 으로 터집니다.
기능까지 채우려면 core-js 를 명시적으로 붙인다npm install core-js
{
"presets": [["@babel/preset-env", {
"targets": "> 0.5%, last 2 versions, not dead",
"useBuiltIns": "usage", // 실제로 쓴 것만 넣는다
"corejs": "3.50" // 설치한 버전을 적는다
}]]
}
"usage" 는 코드를 훑어 실제로 쓴 기능의 폴리필만 넣습니다.
"entry" 는 진입점에 쓴 import 'core-js' 를 대상 브라우저에
맞게 펼치고, 기본값 false 는 아무것도 넣지 않습니다.
대부분 "usage" 가 맞습니다.
5. 바벨이 하지 않는 일 ② — 번들
babel src --out-dir lib 의 결과를 보면 알 수 있습니다.
파일 하나가 파일 하나로 나옵니다. 열 개를 넣으면 열 개가 나옵니다.
합치지 않습니다.
src/index.js → lib/index.js
src/util.js → lib/util.js
src/api.js → lib/api.js
// import 구문은 require 로 바뀌었을 뿐, 파일은 그대로 흩어져 있다
그래서 브라우저에 lib/index.js 하나만 <script> 로
넣으면 동작하지 않습니다. require 는 브라우저에 없는 함수이고,
나머지 파일들은 아무도 불러오지 않습니다. 합치는 일은 번들러의 몫입니다.
| 단계 | 하는 일 | 도구 |
|---|---|---|
| 변환 | 새 문법 → 낮은 문법 | 바벨, SWC, esbuild, tsc |
| 번들 | 흩어진 파일 → 하나(혹은 몇 개) | Vite/Rollup, webpack, esbuild |
| 최소화 | 공백·이름 줄이기, 죽은 코드 제거 | Terser, esbuild, SWC |
실무에서 이 셋을 따로 부르는 일은 거의 없습니다. 번들러가 변환 단계에서 바벨을 불러 쓰는 구조라 설정 한 곳에 다 들어갑니다. 그래서 경계가 안 보이고, 안 될 때 어느 단계 문제인지 짚지 못하게 됩니다. 이 절의 표만 기억하면 그 진단이 됩니다.
6. 2020년 이후 — 지금도 바벨을 쓰는가
크게 두 가지가 바뀌었습니다.
첫째, 변환은 더 빠른 도구로 넘어갔습니다. Go 로 짠 esbuild 와 Rust 로 짠 SWC 가 같은 일을 수십 배 빠르게 합니다. Vite 는 개발 중 변환에 esbuild 를 쓰고, Next.js 는 SWC 를 씁니다. 새 프로젝트에서 바벨을 직접 설정할 일은 거의 없습니다 — 번들러가 이미 골라 놓았습니다.
둘째, 대상 브라우저가 올라갔습니다. ES5 로 낮출 이유가 사라져서, 변환 자체의 비중이 예전만 못합니다.
그럼에도 바벨이 남아 있는 자리가 있습니다.
- 플러그인으로 코드를 다루는 일. esbuild 와 SWC 는 빠른 대신 확장이 제한적입니다. 바벨은 구문 트리(AST)를 열어 주는 도구라, 코드 변환기·자동 마이그레이션· 매크로 같은 것을 짤 수 있습니다. 대규모 코드베이스를 일괄 수정할 때 여전히 이쪽입니다.
- 표준이 되기 전의 문법을 미리 쓰는 일. 제안 단계 문법은 바벨에 먼저 들어옵니다.
- 이미 돌아가는 빌드를 유지하는 일. 2020년쯤 세운 프로젝트라면 지금도 바벨 7 위에 서 있을 가능성이 큽니다.
바벨 8 은 2026년 6월에 나왔습니다. 7 에서 올릴 때는 Node 최소 버전과 기본값 변경을 확인해야 하는데, 대부분의 프로젝트는 번들러를 통해 간접적으로 쓰고 있어 번들러를 올리면 따라옵니다.
7. 한 줄로 줄이면
바벨은 문법을 바꾼다. 기능을 채우지도, 파일을 합치지도 않는다. 폴리필은 core-js 가, 번들은 번들러가 하는 일입니다.
막혔을 때 짚는 순서도 여기서 나옵니다.
문법 오류면 변환 설정(targets)을,
is not a function 이면 폴리필(useBuiltIns)을,
require is not defined 면 번들러를 봅니다.
세 가지가 다른 도구의 일이라는 것만 알아도 헤매는 시간이 크게 줄어듭니다.