본문 바로가기

Babel과 Polyfill

Babel의 문법 변환과 Polyfill의 런타임 API 보완을 구분해 최신 JavaScript의 브라우저 호환 범위를 설계합니다.

ES2015(ES6) 이후 자바스크립트에는 const와 let, 화살표 함수, class, async와 await, 스프레드 문법 같은 기능이 빠르게 더해졌고, React나 Vue 같은 프레임워크도 이런 문법을 적극적으로 씁니다. 하지만 모든 사용자의 브라우저가 최신 문법과 기능을 지원하는 것은 아니어서, 오래된 브라우저나 일부 모바일 브라우저에서는 애플리케이션이 제대로 동작하지 않을 수 있습니다.

이 호환성 문제를 다루는 도구가 Babel과 폴리필(polyfill)입니다. 이 글에서는 Babel이 최신 문법을 대상 브라우저가 이해하는 코드로 바꾸는 방식과, 문법이 아닌 기능을 채워 주는 폴리필이 왜 필요하고 어떻게 쓰는지 정리합니다. 앞 글 Webpack 기초의 설정에 이어, Babel 8과 core-js 3.51을 기준으로 합니다.


Babel이란?

Babel은 최신 자바스크립트 코드를 대상 환경에서 동작하는 자바스크립트 코드로 바꿔 주는 트랜스파일러(transpiler)입니다. 트랜스파일러는 소스 코드를 다른 소스 코드로 바꾸는 도구로, 넓게 보면 컴파일러의 한 종류입니다. 모든 컴파일러가 기계어만 만드는 것은 아닙니다.

Babel 덕분에 개발할 때는 편리한 최신 문법으로 쓰고, 배포할 때는 지원하기로 한 브라우저에서 동작하는 코드로 바꿀 수 있습니다. React의 JSX(<div>Hello</div> 같은 HTML 모양의 코드)는 자바스크립트 표준 문법이 아닌데, Babel이 이를 일반 자바스크립트 함수 호출로 바꿔 줍니다. 타입스크립트의 타입 표기를 지우고 자바스크립트로 바꾸는 데도 Babel을 쓸 수 있습니다.

플러그인과 프리셋

Babel은 플러그인(plugin)과 프리셋(preset)으로 변환을 정합니다. 플러그인은 가장 작은 변환 단위로, 화살표 함수를 일반 함수로 바꾸는 @babel/plugin-transform-arrow-functions처럼 특정 문법 하나를 바꾸는 규칙입니다. 프리셋은 여러 플러그인을 목적에 맞게 미리 묶어 둔 것으로, 개별 플러그인을 하나하나 설정하지 않아도 됩니다.

@babel/preset-env는 가장 널리 쓰이는 프리셋으로, 대상 브라우저를 보고 필요한 변환만 골라 적용하므로 불필요한 변환을 줄여 결과물을 작게 유지합니다. 대상 브라우저는 .browserslistrc 파일이나 package.json의 browserslist 필드로 정하며, Babel 8은 대상을 정하지 않으면 Browserslist의 defaults 쿼리를 기준으로 합니다. @babel/preset-react는 JSX를 자바스크립트로 바꾸고, @babel/preset-typescript는 타입스크립트를 자바스크립트로 바꿉니다.

webpack과 Babel 연결하기

webpack 프로젝트에서 Babel을 쓰려면 babel-loader를 설치하고 설정합니다. 다음 명령의 @babel/core는 변환을 실제로 하는 Babel 본체이고, 두 프리셋은 무엇을 바꿀지 정하며, babel-loader는 webpack이 읽은 자바스크립트 파일을 Babel에 넘겨 줍니다. 모두 빌드할 때만 쓰므로 개발 의존성으로 설치합니다.

npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader

프로젝트 루트의 babel.config.json에 쓸 프리셋을 정합니다. babel-loader가 이 파일을 자동으로 읽습니다.

babel.config.json
{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

대상 브라우저를 더 구체적으로 정하려면 package.json에 browserslist 필드를 더합니다. 기존 package.json에 이 키를 합치는 것입니다.

package.json
{
  "browserslist": [
    "defaults",
    "last 2 versions",
    "> 1%",
    "not IE 11"
  ]
}

이 배열의 쿼리는 합집합으로 대상을 더하고, not IE 11은 IE 11을 뺍니다. defaults의 대상도 함께 들어가므로, 실제로 어떤 브라우저가 대상인지는 설치된 Browserslist로 npx browserslist를 실행해 확인합니다.

webpack.config.js의 module.rules에 babel-loader를 연결합니다. Babel 설정은 babel.config.json 한곳에서 읽으므로 여기에 프리셋을 다시 적지 않습니다.

// webpack.config.js (일부)
module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.jsx?$/, // .js 또는 .jsx 파일
        exclude: /node_modules/, // node_modules 안의 파일은 변환하지 않음
        use: 'babel-loader', // babel.config.json의 설정을 사용
      },
    ],
  },
  // ...
};

이렇게 설정하면 webpack이 자바스크립트 파일을 번들로 묶기 전에, babel-loader를 거쳐 Babel이 최신 문법을 대상 브라우저에 맞는 문법으로 바꿉니다.

더 깊이: 변환한 코드는 명세를 얼마나 정확히 따를까

Babel은 기본으로 변환한 코드가 브라우저의 기본 동작과 최대한 같게 동작하도록 만듭니다. 그런데 명세의 세세한 동작까지 흉내 내려면 도우미 함수가 늘어나, 결과물이 커지거나 느려질 수 있습니다. 예를 들어 클래스 필드는 명세대로라면 Object.defineProperty()로 정의해야 하고, 배열 구조 분해는 이터레이터 프로토콜을 그대로 따라야 합니다.

Babel 7.13.0에 들어온 assumptions 옵션은 이 정확성을 일부 포기하는 대신 더 작은 코드를 얻게 해 줍니다. 내 코드가 어떤 조건을 지킨다고 Babel에 알려 주면 그만큼 단순한 코드를 만듭니다. setPublicClassFields는 클래스 필드가 getter를 가리지 않는다고 보고 단순한 대입으로 바꾸고, iterableIsArray는 구조 분해와 for...of, 스프레드에 쓰는 이터러블이 모두 배열이라고 봅니다. 예전에 플러그인마다 따로 켜던 loose 모드를 대신하는 설정입니다.

Babel 문서는 이런 가정이 명세를 따르지 않으며 예상하지 못한 방식으로 코드를 망가뜨릴 수 있다고 경고합니다. 예를 들어 iterableIsArray를 켠 채 Set이나 제너레이터를 펼치면 배열처럼 다뤄져 잘못된 결과가 나올 수 있습니다. 그래서 크기를 줄이려고 켤 때는 코드 전체가 그 가정을 지키는지 확인하고, 켠 뒤의 결과물을 대상 브라우저에서 다시 시험합니다. 가정의 목록은 Babel의 assumptions 문서에 있습니다.


Polyfill이란?

폴리필은 오래된 브라우저에 없는 최신 기능, 즉 메서드나 객체를 쓸 수 있도록 직접 구현해 둔 코드입니다. Babel이 문법(syntax)을 바꾼다면, 폴리필은 기능(API)을 채워 줍니다.

Babel은 const나 화살표 함수 같은 문법을 바꿀 수 있습니다. 하지만 Promise, Array.prototype.includes, fetch 같은 것은 문법이 아니라 브라우저에 들어 있어야 하는 전역 객체나 메서드입니다. 화살표 함수를 일반 함수로 바꿔도 없는 includes 메서드가 생기지는 않으므로, 이런 기능은 Babel만으로 해결할 수 없고 폴리필이 채워야 합니다.

문법 변환과 API 보완은 따로 확인하고 함께 검증합니다

COMPATIBILITY CHECK

문법 변환과 API 보완은 따로 확인하고 함께 검증합니다

같은 코드가 새 문법과 새 API를 동시에 사용할 수 있습니다. 대상 브라우저에서 부족한 기능마다 필요한 수단을 고르고, 최종 번들을 실제 환경에서 시험합니다.

문법과두종류API의보완수단을비교한다6노드3독립비교관계. 문법parse실패→Babel대상변환,ECMAScriptAPI없음→corejs등,WebAPI없음→별도구현검토다. 세조건은서로배타적인하나의분기가아니며같은앱에함께필요할수있다. 원Babel7설정/지원대상/타입제거≠타입검사 support유지.mobile같다.
문법 parse 실패화살표·JSX 등
연결 관계문법 parse 실패 · 화살표·JSX 등 → 대상 맞춤 변환 · Babel presets
대상 맞춤 변환Babel presets
ECMAScript API 없음Promise·includes
연결 관계ECMAScript API 없음 · Promise·includes → 필요 API 보완 · core-js 등
필요 API 보완core-js 등
Web API 없음fetch 등
연결 관계Web API 없음 · fetch 등 → 별도 구현 검토 · 지원·권한·동작
별도 구현 검토지원·권한·동작

1. 새 문법을 읽지 못함

const add = (a, b) =>
  a + b;

Babel이 대상 환경에 맞는 문법으로 변환합니다. JSX도 해당 프리셋으로 JavaScript 호출로 바꿉니다.

확인: 브라우저가 번들을 parse하는가?

2. ECMAScript API가 없음

[1, 2].includes(2)
new Promise(resolve =>
  resolve(1))

필요한 core-js 등 런타임 구현을 포함합니다. 문법을 바꾸는 것만으로 없는 메서드가 생기지는 않습니다.

확인: 실제 API 호출이 동작하는가?

3. Web API가 없음

fetch('/api/data')

fetch 등은 별도 polyfill이나 대체 구현을 검토합니다. core-js를 넣었다고 모든 Web API가 제공되지는 않습니다.

확인: 기능·권한·동작 조건을 만족하는가?

대상을 정한 뒤 필요한 항목을 모두 적용

문법 변환 + 필요한 API 보완을 함께 적용할 수 있습니다. 이미 지원하는 기능은 불필요하게 보완하지 않습니다. 지원표와 실제 대상 브라우저의 회귀 테스트로 결과를 확인합니다.

이 절의 설정은 Babel 7 기준

preset-env의 useBuiltIns: usage와 corejs 옵션은 Babel 7 예제입니다. Babel 8에서는 이 옵션이 제거되어 별도 polyfill 플러그인을 사용합니다.

설정으로 해결되지 않는 범위도 확인

프레임워크의 지원 환경과 플랫폼 동작을 함께 확인합니다. TypeScript 프리셋의 타입 제거는 타입 검사를 대신하지 않으며, regenerator helper 필요 여부도 버전·변환 설정에 따라 달라집니다.

위 그림은 대상 브라우저에서 부족한 기능을 세 종류로 나눠, 각각에 맞는 수단을 고르는 흐름입니다. 첫째, 브라우저가 화살표 함수나 JSX 같은 새 문법을 읽지 못하면 Babel의 프리셋으로 대상에 맞는 문법으로 바꾸고, 브라우저가 번들을 해석하는지 확인합니다. 둘째, Promise나 includes 같은 ECMAScript API가 없으면 core-js 같은 구현을 넣고, 실제 API 호출이 동작하는지 확인합니다. 셋째, fetch 같은 Web API가 없으면 별도의 폴리필이나 대체 구현을 검토하고, 기능과 권한, 동작 조건을 확인합니다. core-js를 넣었다고 모든 Web API가 생기지는 않습니다.

세 조건은 하나만 고르는 갈림길이 아니라 같은 앱에 함께 필요할 수 있습니다. 대상을 정한 뒤 필요한 항목을 모두 적용하되, 이미 지원하는 기능은 굳이 보완하지 않으며, 지원표와 실제 대상 브라우저에서의 회귀 테스트로 결과를 확인합니다. 그림 아래쪽 카드는 설정만으로 해결되지 않는 범위도 짚습니다. 프레임워크가 지원하는 환경과 플랫폼의 동작을 함께 확인해야 하고, 타입스크립트 프리셋이 타입 표기를 지우는 것은 타입 검사를 대신하지 않습니다.

그림의 다른 카드는 이 글이 예전에 Babel 7을 기준으로 했을 때의 설정을 가리킵니다. Babel 7에서는 @babel/preset-env의 useBuiltIns와 corejs 옵션으로 폴리필을 넣었지만, Babel 8에서 useBuiltIns는 없어졌습니다. 이 글은 Babel 8을 기준으로 하므로, 아래에서 그 대신 babel-plugin-polyfill-corejs3 플러그인을 씁니다(Babel 8 마이그레이션 안내).

core-js로 폴리필 넣기

가장 흔히 쓰는 폴리필은 core-js로, 안정된 ECMAScript 기능 대부분의 폴리필을 제공합니다. 예전의 @babel/polyfill은 더 이상 쓰지 않고 core-js를 직접 씁니다. core-js는 실제로 서비스되는 코드에 들어가므로 dependencies에 설치하고, 폴리필을 자동으로 넣어 줄 Babel 플러그인은 빌드 도구라서 devDependencies에 설치합니다.

npm install core-js
npm install --save-dev babel-plugin-polyfill-corejs3

core-js를 설치했다고 폴리필이 저절로 번들에 들어가지는 않습니다. 다음처럼 엔트리 파일에서 직접 가져오거나, 뒤에서 볼 Babel 플러그인 설정으로 연결해야 합니다.

가장 단순한 방법은 엔트리 파일의 맨 위에서 안정된 기능 전체를 가져오는 것입니다. import 'core-js/stable'은 아무 이름도 가져오지 않고 모듈을 실행만 하는 형태로, 실행되면서 빠진 기능을 전역 객체에 채워 넣습니다. 다른 코드가 그 기능을 쓰기 전에 실행되어야 하므로 맨 위에 둡니다.

// src/index.js
import 'core-js/stable'; // 안정된 ECMAScript 기능 전체의 폴리필

import React from 'react';
import ReactDOM from 'react-dom/client';
// ... 나머지 코드

이 방법은 대상 브라우저가 이미 지원하는 기능의 폴리필까지 모두 번들에 넣으므로 결과물이 커질 수 있습니다.

필요한 폴리필만 자동으로 넣기

babel-plugin-polyfill-corejs3를 usage-global 방식으로 쓰면, Babel이 코드를 바꾸는 동안 실제로 쓴 기능 가운데 대상 브라우저가 지원하지 않는 것에만 폴리필을 자동으로 넣습니다. 이 경우 엔트리 파일에 import 'core-js/stable'을 적지 않습니다.

babel.config.json
{
  "presets": ["@babel/preset-env", "@babel/preset-react"],
  "plugins": [
    ["polyfill-corejs3", { "method": "usage-global", "version": "3.51" }]
  ]
}

method는 폴리필을 넣는 방식입니다. usage-global은 코드에서 쓴 기능을 보고 전역에 폴리필을 넣고, entry-global은 엔트리에 직접 적은 import 'core-js'를 대상에 필요한 것만 가져오도록 바꿉니다. 라이브러리를 만들 때처럼 전역을 건드리지 않아야 한다면 usage-pure를 씁니다. version은 설치한 core-js의 버전으로, 기본값 "3.0"에는 최근의 폴리필이 없으므로 실제로 설치한 버전을 마이너 버전까지 적고, core-js를 업데이트하면 이 값도 함께 바꿉니다(babel-plugin-polyfill-corejs3 문서). 아직 표준이 아닌 제안 단계의 기능까지 넣으려면 proposals: true를 더하지만, 모든 제안 기능이 언제든 제공된다는 뜻으로 읽지는 않습니다.

이 설정은 browserslist의 대상과 함께 동작해, 대상 브라우저에 따라 필요한 폴리필만 넣습니다. 쓰는 기능과 대상 환경을 기준으로 넣을 범위를 줄이는 방법일 뿐, 모든 프로젝트에서 이 옵션이 항상 가장 알맞다고 단정하지는 않습니다.

async 함수나 제너레이터를 오래된 브라우저용으로 바꿀 때 Babel이 만든 코드는 자체 도우미를 쓰므로, 보통 regenerator-runtime 패키지를 따로 설치하지 않아도 됩니다. 다만 Babel 8은 코드에 직접 적힌 전역 regeneratorRuntime을 더 이상 도우미로 바꿔 주지 않으므로, 그런 오래된 코드가 있다면 엔트리에서 regenerator-runtime을 가져오거나 babel-plugin-polyfill-regenerator를 검토합니다.


Babel과 폴리필 비교

구분Babel (트랜스파일러)폴리필 (실행할 때 기능 보완)
대상최신 문법최신 기능과 API
예화살표 함수, const, let, class, JSXPromise, fetch, Array.prototype.includes, Map
방식코드를 대상 환경이 읽을 수 있는 문법으로 바꿈없는 기능을 구현한 코드를 실행할 때 더함
해결하는 문제문법 오류로 번들을 해석하지 못함ReferenceError, TypeError 같은 실행 중 오류

대부분의 웹 프로젝트는 최신 문법과 새 API를 함께 쓰므로, Babel과 폴리필을 같이 써서 정해 둔 대상 브라우저의 문법과 API 지원 범위를 맞춥니다. fetch 같은 Web API는 core-js가 아니라 별도의 보완이 필요할 수 있고, 프레임워크가 지원하는 환경과 실제 브라우저 테스트도 확인해야 합니다. 호환성 설정은 한 번 정하고 끝나는 값이 아니라, 지원할 브라우저와 번들 크기 사이에서 계속 조정하는 기준입니다.


정리

  • Babel은 최신 문법과 JSX를 대상 브라우저가 읽을 수 있는 코드로 바꾸는 트랜스파일러이고, 폴리필은 브라우저에 없는 API를 채우는 코드입니다.
  • @babel/preset-env는 Browserslist로 정한 대상에 필요한 변환만 적용하며, Babel 8은 대상을 정하지 않으면 defaults를 기준으로 합니다.
  • 문법 변환만으로 없는 메서드나 객체가 생기지는 않으므로, ECMAScript API는 core-js로, fetch 같은 Web API는 별도로 보완합니다.
  • Babel 8에서는 useBuiltIns 대신 babel-plugin-polyfill-corejs3의 usage-global로 실제로 쓴 기능의 폴리필만 넣고, version에 설치한 core-js의 마이너 버전을 적습니다.
  • Babel 설정은 한곳에 두고, 설정을 바꾼 뒤에는 새로 빌드한 결과물을 실제 대상 브라우저에서 시험합니다.

연습 문제

1. 필요한 준비 고르기

가상의 대상 브라우저가 화살표 문법은 지원하지만 Array.prototype.includes와 fetch는 지원하지 않는다고 가정합니다. 이 조건에서 화살표 함수를 바꿔야 할까요? includes와 fetch에는 각각 무엇이 필요할까요?

해설 보기

화살표 함수는 이미 지원하므로 바꿀 필요가 없습니다. includes는 ECMAScript API라서 core-js 같은 폴리필로 채우고, fetch는 Web API라서 core-js가 아닌 별도의 폴리필이나 대체 구현을 검토합니다.

화살표를 function 문법으로 바꾸는 일만으로는 두 API가 생기지 않습니다. 이 지원 여부는 문제를 위한 가정이며, 실제 브라우저의 지원 목록은 지원표와 테스트로 확인해야 합니다.

2. 세 가지가 모두 부족한 대상

다른 대상 브라우저는 화살표 문법을 읽지 못하고, Promise와 fetch도 없습니다. 어떤 세 가지 준비가 필요할까요? @babel/preset-typescript로 타입 표기를 지우는 데 성공했다면 타입 오류가 없다고 할 수 있을까요?

해설 보기

화살표 문법은 Babel로 바꾸고, Promise 같은 ECMAScript API는 core-js로 채우며, fetch는 별도의 Web API 보완을 따로 판단합니다. 세 가지를 각각 준비해야 합니다.

타입 표기를 지우는 것과 타입을 검사하는 것은 다른 일이므로, Babel 변환이 성공했다고 타입 오류가 없는 것은 아닙니다. tsc --noEmit 같은 별도의 타입 검사를 함께 실행해야 합니다.

3. 대상을 바꾼 뒤의 확인

browserslist의 대상 목록을 바꾼 뒤, 예전에 만들어 둔 bundle.js만 대상 브라우저에서 시험해 문제가 없었습니다. 바뀐 설정이 검증되었다고 할 수 있을까요? 또 browserslist 설정을 아예 지웠더니 오래된 브라우저용 변환이 사라졌다면, 그 이유는 무엇일까요?

해설 보기

검증되었다고 할 수 없습니다. 예전 번들은 예전 설정으로 만든 결과이므로, 새 설정으로 다시 빌드한 결과물을 실제 대상 브라우저에서 시험해야 바뀐 정책을 확인한 것입니다.

설정을 지우면 Babel 8은 Browserslist의 defaults 쿼리를 기준으로 하므로, defaults에 들어가지 않는 오래된 브라우저를 위한 변환은 하지 않습니다. 오래된 브라우저를 지원해야 한다면 대상을 직접 정하고, npx browserslist로 실제 목록을 확인합니다.

@babel/preset-env의 옵션은 Babel의 preset-env 문서에서 확인할 수 있습니다.

이것으로 14장을 마칩니다. 14장에서는 Git으로 변경 이력을 관리하고 협업하는 방법, npm으로 패키지와 버전을 관리하는 방법, webpack으로 모듈을 번들로 묶는 방법, 그리고 Babel과 폴리필로 브라우저 호환성을 맞추는 방법까지 개발 도구와 작업 흐름을 살펴봤습니다. 다음 장의 Progressive Web Apps (PWA) 소개부터는 PWA를 시작으로 WebAssembly, JAMstack 아키텍처, 실시간 웹 통신까지 최신 웹 기술의 흐름을 다룹니다.