RainyLab
Dev · Build

바벨은 무엇을 하고 무엇을 하지 않는가

바벨을 이해하는 가장 빠른 방법은 결과물을 열어 보는 것입니다. 한 파일을 넣고 나온 파일을 열면 무슨 일을 했는지 한눈에 보입니다. 그런데 그때 같이 알게 되는 것이 하나 더 있습니다 — 바벨은 생각보다 훨씬 적은 일만 합니다. 합치지도 않고, 없는 기능을 채워 주지도 않습니다. 이 경계를 모르면 "빌드는 됐는데 브라우저에서 안 돈다"에서 며칠을 씁니다.

Archive · 2020

원문은 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;

바뀐 것을 하나씩 짚으면 바벨이 하는 일이 전부 드러납니다.

전부 문법을 다른 문법으로 바꾼 것입니다. 이게 바벨의 정체입니다 — 같은 뜻의 코드를 더 낮은 버전의 문법으로 다시 쓰는 도구입니다.

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년에는 그게 안전한 기본값이었습니다. 지금은 손해입니다.

package.json 에 적어 두면 다른 도구도 같은 값을 읽는다"browserslist": [
  "> 0.5%",
  "last 2 versions",
  "not dead"
]

browserslist 는 바벨 전용이 아닙니다. Autoprefixer 도, 번들러도 같은 값을 읽습니다. 여기 한 곳에 적어 두면 도구마다 대상이 어긋나는 일이 없어집니다. 실제로 무엇이 켜지는지는 다음 명령으로 볼 수 있습니다.

npx browserslist

4. 바벨이 하지 않는 일 ① — 폴리필

가장 자주 걸리는 오해입니다. preset-env 는 문법만 바꿉니다. 없는 기능을 만들어 주지는 않습니다.

바벨이 바꾸는 것 — 문법
  • 화살표 함수, class
  • 구조 분해, 스프레드
  • 템플릿 리터럴
  • async/await, 옵셔널 체이닝
바벨이 안 만드는 것 — 기능
  • Promise, Map, Set
  • Array.prototype.includes
  • Object.entries
  • fetch, 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 로 낮출 이유가 사라져서, 변환 자체의 비중이 예전만 못합니다.

그럼에도 바벨이 남아 있는 자리가 있습니다.

바벨 8 은 2026년 6월에 나왔습니다. 7 에서 올릴 때는 Node 최소 버전과 기본값 변경을 확인해야 하는데, 대부분의 프로젝트는 번들러를 통해 간접적으로 쓰고 있어 번들러를 올리면 따라옵니다.

7. 한 줄로 줄이면

바벨은 문법을 바꾼다. 기능을 채우지도, 파일을 합치지도 않는다. 폴리필은 core-js 가, 번들은 번들러가 하는 일입니다.

막혔을 때 짚는 순서도 여기서 나옵니다. 문법 오류면 변환 설정(targets)을, is not a function 이면 폴리필(useBuiltIns)을, require is not defined 번들러를 봅니다. 세 가지가 다른 도구의 일이라는 것만 알아도 헤매는 시간이 크게 줄어듭니다.