Posts Babel 기초
Post
Cancel

Babel 기초

바벨(Babel 7) 기본

자바스크립트 문법은 빠르게 발달 되어가고 있습니다. 하지만 자바스크립트를 실행하는 환경은 이를 받쳐주지 못하는 경우가 많습니다. 그러한 이유로는 브라우저의 종류가 다양하여 모든 브라우저에서 동일하게 문법을 지원하는것이 아니기 때문입니다. 또한 Node.js의 경우 버전에 따라 지원하는 문법이 다르기때문에 브라우저와 동일한 문제를 가지고 있습니다.

Babel: JavaScript transpiler

이러한 문제를 해결하기 위해 등장한 것이 바로 자바스크립트 트랜스파일러(transpiler)인 바벨입니다. 바벨을 이용하면 ES6 이상의 최신 문법으로 작성한 자바스크립트 코드를 ES5 이하의 예전 문법으로작성한 것 처럼 소스 코드 내의 문법의 형태를 변경할 수 있습니다. 이렇게 Babel를 통해 문법 평태가 바뀐 소스 코드는 최신 문법을 지원하는 실행 환경 뿐만 아니라 아직 최신 문법들이 적용 되지 않은 실행 환경에서도 문제없이 작동하게 됩니다.

TypeScript와 JSX지원

Babel은 ES6 이상의 최신 문법 뿐만 아니라 TypeScript나 JSX로 작성된 코드를 변환할 때도 많이 사용됩니다. React는 일반적으로 JSX라는 특수한 문법을 사용하여 코딩을 하기 때문에 개발자가 작성한 원본 코드는 브라우저에서 제대로 실행이 되지 않습니다. 따라서 보통 Webpack 번들러와 Babel 로더를 이용하여 React 프로젝트를 빌드합니다.

바벨의 기본 동작

바벨은 ECMAScript2015 이상의 코드를 적당한 하위 버전으로 바꾸는 것이 주된 역활이다. 이렇게 바뀐 코드는 인터넷 익스플로러나 구버전 브라우저처럼 최신 자바스크립트 코드를 이해하지 못하는 환경에서도 잘 작동한다.

먼저 Babel 최신 버전을 설치한다. 터미널 도구를 사용하기 위해 커맨드라인 도구도 함께 설치한다.

1
2
3
4
mkdir babel-example
cd babel-example
npm init -y
npm install -D @babel/core @babel/cli

설치를 완료후 node_modules/.bin 폴더에 추가된 바벨 명령어를 사용할 수 있다.

  • app.js
1
const alert = msg => window.alert(msg)
1
2
➜  babel-example git:(master) ✗ npx babel app.js
const alert = msg => window.alert(msg);

위의 결과에서 어떠한 변화도 없다….

Babel은 세 단계로 빌드를 진행한다.

  1. 파싱(Parsing)
  2. 변환(Transforming)
  3. 출력 (Printing)

코드를 읽고 추상 구문 트리(AST)로 변환하는 단계를 “파싱”이라고 한다. 이것은 빌드 작업을 처리하기에 적합한 자료구조인데 컴파일러 이론에 사용되는 개념이다. 추상 구문 트리를 변경하는 것이 “변환” 단계이다. 실제로 코드를 변경하는 작업을 한다. 변경된 결과물을 “출력”하는 것을 마지막으로 바벨은 작업을 완료한다.

플러그인

기본적으로 바벨은 코드를 받아서 코드를 반환한다. 바벨 함수를 정의한다면 이런 모습이 될 것이다.

1
const babel = code => code

바벨은 파싱출력만 담당하고 가운데의 변환작업은 다른 녀석이 처리하는데 이것이 플러그인 이다.

Custom Plugin

Plugin을 직접 만들면서 동작 원리를 살펴 보자. Myplugin.js 라는 파일을 아래처럼 만들어보자

[출처:바벨 홈페이지 코드]

  • myplugin.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = function myplugin() {
        return {
    visitor: {
      Identifier(path) {
        const name = path.node.name;
				// 바벨이 만든 AST 노드를 출력한다
        console.log("Identifier() name:", name)
        // 이름의 문자열을 역순으로 설정 홍길동 => 동길홍
        path.node.name = name
                .split("")
                .reverse()
                .join("");
      },
    },
  };
}

Plugin 형식은 vistor 객체를 가진 함수를 반환해야 한다. 이 객체는 Babel이 파싱하여 만든 추상 구문 트리(AST)에 접근할 수 있는 메소드를 제공한다. 그 중 Identifier() 메소드의 동작 원리를 살펴보았다.

Plugin 사용법을 알아보자.

1
2
npx babel --help
--plugins [list]                            A comma-separated list of plugin names.

--plugins 옵션에 plugin을 추가하면 된다.

1
2
3
4
5
6
7
8
9
npx babel app.js --plugins ./myplugin.js

Identifier() name: alert
Identifier() name: msg
Identifier() name: window
Identifier() name: alert
Identifier() name: msg

const trela = gsm => wodniw.trela(gsm);

Identifier() 메소드로 들어온 인자 path에 접근하면 코드 조각에 접근할 수 있는 것 같다. path.node.name의 문자열을 역전하는것이 결과로 보인다.

ECMASCript2015로 작성한 코드를 인터넷 익스플로러에서 돌리려면 const 코드를 var로 변경이 필요하다. 그러한 plugin을 만들어보자

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = function myplugin() {
        return {
    visitor: {
      VariableDeclaration(path) {
        console.log("VariableDeclaration() kind:", path.node.kind) // const
				if (path.node.kind === "const") {
          path.node.kind = "var"
        }
      },
    },
  };
}

이번에는 vistor 객체에 VariableDeclaration() 메소드를 정의했다. path에 접근해 보면 키워드가 잡히는 것을 알 수 있다. path.node.kind가 const 일 경우 var로 변환해주는 plugin이다.

이 plugin으로 다시 빌드해보자.

1
2
3
npx babel app.js --plugins ./myplugin.js 
VariableDeclaration() kind: const
var alert = msg => window.alert(msg);

마지막 줄을 보면 constvar로 바뀐걸 확인 할 수 있다.

Plugin 사용하기

위의 plugin과 같은 결과를 만드는 것이 block-scoping 플러그인이다. const, let 과 같은 예약어를 var로 변경한다.

NPM 패키지로 제공하는 plugin을 설치하고,

1
npm install -D @babel/plugin-transform-block-scoping

설치한 plugin을 사용해보자.

1
2
3
npx babel app.js --plugins @babel/plugin-transform-block-scoping

var alert = msg => window.alert(msg);

위에서 작성한 plugin과 동일한 결과가 나온다.

인터넷 익스플로러는 화살표 함수도 지원하지 않아서 여전히 app.js 를 사용할 수 없다. 그래서 arrow-functions플러그인을 이용해서 일반 함수로 변경할 수 있다.

1
2
3
4
5
6
7
8
9
npm install -D @babel/plugin-transform-arrow-functions

npx babel app.js \
  --plugins @babel/plugin-transform-block-scoping \
  --plugins @babel/plugin-transform-arrow-functions
  
var alert = function (msg) {
  return window.alert(msg);
};

ECMAScript5에서부터 지원하는 엄격 모드를 사용하는 것이 안전하기 때문에 "use strict"구문을 추가하자. 추가를 하기 위해 strict-mode플러그인을 이용

그 전에 커맨드라인 명령어가 점점 길어지기 때문에 설정 파일로 분리하는 것이 좋을듯 싶다. Webpack가 webpack.config.js 를 기본 설정 파일로 사용하듯 Babel 또한 babel.config.js 를 기본 설정파일로 사용된다.

프로젝트 루트에 babel.config.js 파일을 아래와 같이 작성하자.

  • babel.config.js
1
2
3
4
5
6
7
module.exports = {
  plugins: [
    "@babel/plugin-transform-block-scoping",
    "@babel/plugin-transform-arrow-functions",
    "@babel/plugin-transform-strict-mode",
  ],
}

커맨드라인에서 사용한 block-scoping, arrow-functions 플러그인을 설정 파일로 옮겼는데 plugins 배열에 추가하는 방식이다. strict-mode 플러그인을 마지막 줄에 추가 하였다.

다시 빌드 해보자

1
2
3
4
5
6
npx babel app.js
"use strict";

var alert = function (msg) {
  return window.alert(msg);
};

상단에 “use strict” 구문이 추가되어 엄격모드가 활성화 되었다. 이제야 비로소 인터넷 익스플로러에서ㅗ 안전하게 동작하는 코드로 트랜스파일 하였다.

이처럼 변환을 위한 플러그인 목록은 공식 문서의 Plugins에서 확인 가능하다.

프리셋

ECMAScript2015+으로 코딩할 때 필요한 플러그인을 일일이 설정하는 일은 무척 오래된 일이다. 코드 한 줄 작성하는데도 세 개 플러그인 셋팅을 했으니 말이다. 여기 목적에 맞게 플러그인을 모아둔 것이 있다 그것이 바로 “프리셋”!

커스텀 프리셋

우선 위에서 사용한 세 개 플러그인을 하나의 프리셋으로 만들어 보자. mypreset.js 파일을 다음과 같이 작성하자.

  • mypreset.js
1
2
3
4
5
6
7
8
9
module.exports = function mypreset() {
  return {
    plugins: [
      "@babel/plugin-transform-arrow-functions",
      "@babel/plugin-transform-block-scoping",
      "@babel/plugin-transform-strict-mode",
    ]
  }
}

plugins 배열에 사용한 세 개 플러그인을 담았다.

프리셋을 사용하기 위해 바벨 설정을 바꾸자

  • babel.config.js
1
2
3
module.exports = {
  presets: ["./mypreset.js"],
}

babel.config.jsplugins배열을 제거하고 presets에 방금 만든 mypreset.js를 추가하였다. 실행을 해보면 plugins와 동일한 결과를 출력할 것이다.

프리셋 사용하기

이처럼 바벨은 목적에 따라 몇 가지 프리셋을 제공한다.

  • preset-env
  • preset-flow
  • preset-react
  • preset-typescript

preset-env는 ECMAScript2015+를 변환할 때 사용된다. 바벨 7 이전 버전에는 연도별로 각 프리셋을 제공 하였지만(babel-reset-es2015, babel-reset-es2016, babel-reset-es2017, babel-reset-latest) 지금은 env 하나로 합쳐졌다.

preset-flow, preset-react, preset-typescript는 flow, react, typescript를 변환시키기 위한 프리셋이다.

인터넷 익스플로러 지원을 위해 env 프리셋을 사용해보자. 먼저 패키지를 다운로드 한다.

1
npm i -D @babel/preset-env

babel.config.jspresets값을 바꾸자.

  • babel.config.js
1
2
3
module.exports = {
  presets: ["@babel/preset-env"],
}

그리고 빌드를하면

1
2
3
4
5
6
npx babel app.js
"use strict";

var alert = function alert(msg) {
  return window.alert(msg);
};

우리가 만든 mypreset.js와 같은 결과를 출력한다.

env 프리셋 설정과 폴리필(polyfill)

과거에 제공했던 연도별 프리셋을 사용해 본 경험이 있다면 까다롭고 헷갈리는 설정 때문에 애를 먹었을지도 모르겠다. 그에 비해 env 프리셋은 무척 단순하고 직관적인 사용법을 제공한다.

타겟 브라우저

우리 코드가 크롬 최신 버전(2021년 4월 기준)만 지원한다 하자. 그렇다면 인터넷 익스플로러를 위한 코드 변환은 불필요하다. target 옵션에 브라우저, 버전명만 지정하면 env 프리셋은 이에 맞는 플러그인들을 찾아 최적의 코드를 출력해 낸다.

  • babel.config.js
1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
	presets: [
		[
			"@babel/preset-env",
			{
				targets: {
					chrome: "90",
				},
			},
		],
	],
}
1
2
3
4
npx babel app.js
"use strict";

const alert = msg => window.alert(msg);

크롬은 블록 스코핑과 화살표 함수를 지원하기 때문에 코드를 변환하지 않고 이러한 결과물을 만들어냈다. 만약 인터넷 익스플로러도 지원해야 한다면 바벨 설정에 브라우저 정보 하나 더 추가하면 된다.

  • babel.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
	presets: [
		[
			"@babel/preset-env",
			{
				targets: {
					chrome: "90",
					ie: "11", // ie 11까지 지원하는 코드를 만든다
				},
			},
		],
	],
}

인터넷 익스플로러에 대응하는 코드로 변환 된다.

폴리필(polyfill)

이번엔 변환과 조금 다른 폴리필(polyfill)에 대해 알아보자

Polyfill : 직역하면 충전솜이라는 의미가 된다. → 솜이 꺼졋을때 메꿔주는 역활을 한다.

ECMAScript2015의 Promise 객체를 사용한 코드이다.

  • app.js
1
new Promise()

바벨로 처리하면 어떤 결과가 나올까?

1
2
3
4
npx babel app.js
"use strict";

new Promise();

env 프리셋으로 변환을 시도했지만 Promise는 그대로 변함이 없다. target에 ie 11을 설정하고 빌드한 것인데 인터넷 익스플로러는 여전히 프러미스를 해석하지 못하고 에러를 던진다.

브라우저는 현재 스코프부터 시작해 전역까지 Promise라는 이름을 찾으려고 시도할 것이다. 그러나 스코프 어디에도 Promise란 이름이 없기 때문에 레퍼런스 에러를 발생하고 프로그램이 죽은 것이다.

플러그인이 프러미스를 ECMAScript5 버전으로 변환할 것으로 기대했는데 예상과 다르다. 바벨은 ECMAScript2015+를 ECMAScript5 버전으로 변환할 수 있는 것만 빌드한다. 그렇지 못한 것들은 폴리필(polyfill)이라고 부르는 코드조각을 추가해서 해결한다.

가령 ECMAScript2015의 블록 스코핑은 ECMAScript5의 함수 스코핑으로 대체할 수 있다. 화살표 함수 또한 일반 함수로 대체할 수 있다. 이런 것들은 바벨이 변환해서 ECMAScript5 버전으로 결과물을 만든다.

한편 프러미스는 ECMAScript5 버전으로 대체할 수 없다. 다만 ECMAScript5 버전으로 구현할 수는 있다.(참고: core-js promise).

env 프리셋은 폴리필을 지정할 수 있는 옵션을 제공한다.

  • babel.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        useBuiltIns: "usage", // 폴리필 사용 방식 지정
        corejs: {
          // 폴리필 버전 지정
          version: 2,
        },
      },
    ],
  ],
}

useBuiltIns는 어떤 방식으로 폴리필을 사용할지 설정하는 옵션이다. usage, entry, false 세가지 값을 사용하는데 기본값이 false 이므로 폴리필이 동작하지 않았던 것이다. 반면 usageentry를 설정하면 폴리필 패키지 중 core-js를 모듈로 가져온다(이전에 사용하던 babel/polyfill은 바벨 7.4.0부터 사용하지 않음).

core-js 모듈의 버전도 명시하는데 기본값은 2다. 버전 3과 차이는 확실히 잘 모르겠다. 이럴 땐 그냥 기본값을 사용하는 편이 좋다.

자세한 폴리필 옵션은 바벨 문서의 useBuiltInscorejs 섹션을 참고하자.

폴리필이 추가된 결과물을 확인해 보자.

1
2
3
4
5
6
7
8
npx babel app.js
"use strict";

require("core-js/modules/es6.object.to-string.js");

require("core-js/modules/es6.promise.js");

new Promise();

Core-js 패키지로부터 프러미스 모듈을 가져오는 임포트 구문이 상단에 추가되었다. 이제야 비로소 인터넷 익스플로러에서 안전하게 돌아가는 결과물을 만들었다.!!

웹팩으로 통합

실무 환경에서는 바벨을 직접 사용하는 것보다는 웹팩으로 통합해서 사용하는 것이 일반적이다. 로더 형태로 제공하는데 babel-loader이 그것이다.

먼저 패키지를 설치하자.

1
npm i -D babel-loader

웹팩 설정에 로더를 추가한다.

  • webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
module.exports = {
	module: {
		rules: [
			{
				test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader", // 바벨 로더를 추가한다
       },
    ],
  },
}

.js 확장자로 끝나는 파일은 babel-loader가 처리하도록 설정했다. 사용하는 써드파티 라이브러리가 많을 수록 바벨 로더가 느리게 동작할 수 있는데 node_modules 폴더를 로더가 처리하지 않도록 예외 처리를 했다. (참고)

폴리필 사용 설정을 했다면 core-js도 설치해야한다. 웹팩은 바벨 로더가 만든 아래 코드를 만나면 core-js를 찾을 것이기 때문이다.

1
2
require("core-js/modules/es6.promise")
require("core-js/modules/es6.object.to-string")

버전 2로 패키지를 설치하자.

1
npm i core-js@2
This post is licensed under CC BY 4.0 by the author.