바벨의 기본 개념 - 프론트엔드 개발환경의 이해 : Babel
1. 배경
1.1 크로스 브라우징
사용하는 말이 달라서 바벨탑이 실패했듯이, 브라우저마다 사용하는 언어가 달라서 프론트엔드 코드는 일관적이지 못할 때가 많다.
고대 히브리 신화를 보면 바벨탑이라는 것이 있다.
사람들이 하늘에 가까워지기 위해 바벨탑을 쌓았다.
그런데 하느님이 이것을 보고 사람들의 ‘언어’를 달리하여 사람들이 서로 소통하지 못하게 했다.
그래서 바벨탑 쌓는 것을 실패하게 되었다.
스팩과 브라우저가 개선되고 있지만, 여전히 IE는 Promise 를 이해하지 못한다.
작년까지만 해도 사파리 최신 브라우저는 Promise.prototype.finally
메서드를 사용할 수 없었다.
그래서
Promise.prototype.finally
에 대한 폴리필 코드를 추가하여 해결하는 방식으로 작업했다.
프론트엔드 개발에서 크로스브라우징 이슈는 코드의 일관성을 해치고 초심자를 불안하게 만든다.
히브리어로 바벨이 ‘혼돈’이란 뜻인 것처럼 말이다.
크로스브라우징의 혼란을 해결해 줄 수 있는 것이 바벨이다.
ECMAScript2015+로 작성한 코드를 모든 브라우저에서 동작하도록 호환성을 지켜준다.
타입스크립트, React의 JSX 처럼 다른 언어로 분류되는 것도 포함한다.
1.2 트랜스파일과 빌드
이렇게 변환하는 것을 ‘트랜스파일’ 한다고 표현한다.
변환 전후의 추상화 수준이 다른 빌드와는 달리 트랜스파일은 추상화 수준을 유지한 상태로 코드를 변환한다.
- 타입스크립트 -> 자바스크립트
- JSX -> 자바스크립트
이렇게 트랜스파일 후에도 여전히 코드를 읽을 수 있다.
그런데 요즘에는 빌드 / 트랜스파일을 딱히 구분하지 않고 사용하는 것 같다.
2. 바벨의 기본 동작
바벨을 먼저 설치해보고 바벨이 어떤 동작을하고 어떤 결과를 보여주는지 확인해보도록 하겠다.
바벨은 ECMAScript2015+ 이상의 코드를 적당한 하위 버전으로 바꾸는 것이 주된 역할이다.
이렇게 바뀐 코드는 IE나 구버전 브라우저처럼 최신 자바스크립트 코드를 이해하지 못하는 환경에서도 잘 동작한다.
npm i -D @babel/core @babel/cli
바벨은 코어가 있고, 또 터미널도구로 사용하기 위해 cli
를 설치해야 한다.
그리고 바벨로 변환시킬 예시 코드를 작성해보자.
// app.js
const alert = msg => window.alert(msg);
const
는 ES6의 문법이다.
상수를 정의하기 위한 키워드이다.
위에서 const
뿐만아니라 애로우 함수도 사용했는데, 이것도 ES6 문법이다.
ES6 문법을 인식 못하는 IE에선 작동을 안하는 문법이다.
바벨을 통해 위와 같은 코드를 모든 브라우저에서 인식하도록 변환해보자.
node_modules/.bin/babel
모듈을 설치하면 node_modules
폴더 안에 .bin
폴더 안에 해당 모듈이 들어가게 된다.
그래서 위와 같이 해당 경로로 들어가 해당 모듈을 실행시켜줘도 된다.
이렇게 바벨을 실행해도되고 아니면, npx
라는 명령어를 활용해 간단하게 모듈을 실행시킬 수도 있다.
npx babel app.js
이렇게하면 위와 같이 결과가 나온 것을 확인할 수 있다.
바벨로 변환하기 이전과 이후 코드가 똑같다.
어떻게된 것일까?
바벨은 세 단계로 빌드를 진행한다.
- 파싱(Parsing)
- 변환(Transforming)
- 출력(Printing)
코드를 읽고 추상 구문 트리(AST)로 변환하는 단계를 파싱이라고 한다.
이것은 빌드 작업을 처리하기에 적합한 자료구조인데 컴파일러 이론에 사용되는 개념이다.
파싱 단계에선 코드를 받으면 코드를 token 단위로 다 분해를 한다.
위의 코드를 예로들면,
const
라는 토큰 /alert
이라는 토큰 /=
토큰 …
이렇게 하나하나 다 분해한다.
추상 구문 트리를 변경하는 것이 변환 단계이다.
실제로 코드를 변경하는 작업을 한다.
변경된 결과물을 출력하는 것을 마지막으로 바벨은 작업을 완료한다.
하지만 방금 결과를 보면 파싱은 한 것 같은데 변환은 하지 않았다.
그리고 출력했다.
3. 플러그인
기본적으로 바벨은 코드를 받아서 코드를 반환한다.
바벨 함수를 정의한다면 이런 모습이 될것이다.
const babel = code => code;
바벨은 파싱과 출력만 담당하고 변환작업은 다른 녀석이 처리하는데 이것을 플러그인
이라고 부른다.
바벨에는 플러그인이라는 요소가 있는데, 이 플러그인이 바로 변환을 담당하는 요소이다.
웹팩에서 커스텀으로 로더도 만들고 플러그인도 만들었듯이 바벨 플러그인도 커스텀으로 만들 수 있다.
커스텀 플러그인을 만들어보면서 플러그인이 어떻게 변환작업을 진행하는지 살펴보도록 하겠다.
커스텀 플러그인
// my-babel-plugin.js
module.exports = function myBabelPlugin() {
return {
visitor: {
Identifier(path) {
const name = path.node.name;
// 바벨이 만든 AST 노드를 출력한다.
console.log('Identifier() name:', name);
// 변환작업: 코드 문자열을 역순으로 변환한다.
path.node.name = name
.split("")
.reverse()
.join("");
}
}
}
}
커스텀 플러그인을 만들땐 visitor
라는 객체를 가지고있는 객체를 반환해줘야 된다.
위 커스텀 플러그인엔 Identifier
라는 메서드가 들어있다.
이 Identifier
는 path
라는 객체를 받는다.
이 path
가 바벨이 넣어줘야될 객체이다.
이 path
라는 객체는 path.node.name
으로 파싱된 결과물에 접근할 수 있다.
일단 파싱된 결과물을 console.log
로 찍어보았다.
// my-babel-plugin.js
module.exports = function myBabelPlugin() {
return {
visitor: {
Identifier(path) {
const name = path.node.name;
// 바벨이 만든 AST 노드를 출력한다.
console.log('Identifier() name:', name);
// 변환작업: 코드 문자열을 역순으로 변환한다.
// path.node.name = name
// .split("")
// .reverse()
// .join("");
}
}
}
}
일단 위에처럼 주석처리하고 console.log
만 찍어보자.
npx babel --help
우선 바벨의 헬프 문서를 보면서 어떻게 플러그인을 써보는지 알아보도록 하자.
USER@DESKTOP-RUCOU5S MINGW64 /d/webpack/babel (master)
$ npx babel --help
Usage: babel [options] <files ...>
Options:
-f, --filename [filename] The filename to use when reading from stdin. This will be used in source-maps, errors etc.
--presets [list] A comma-separated list of preset names.
--plugins [list] A comma-separated list of plugin names.
--config-file [path] Path to a .babelrc file to use.
--env-name [name] The name of the 'env' to use when loading configs and plugins. Defaults to the value of BABEL_ENV, or else NODE_ENV, or else 'development'.
--root-mode [mode] The project-root resolution mode. One of 'root' (the default), 'upward', or 'upward-optional'.
--source-type [script|module]
--no-babelrc Whether or not to look up .babelrc and .babelignore files.
--ignore [list] List of glob paths to **not** compile.
--only [list] List of glob paths to **only** compile.
--no-highlight-code Enable or disable ANSI syntax highlighting of code frames. (on by default)
--no-comments Write comments to generated output. (true by default)
--retain-lines Retain line numbers. This will result in really ugly code.
--compact [true|false|auto] Do not include superfluous whitespace characters and line terminators.
--minified Save as many bytes when printing. (false by default)
--auxiliary-comment-before [string] Print a comment before any injected non-user code.
--auxiliary-comment-after [string] Print a comment after any injected non-user code.
-s, --source-maps [true|false|inline|both]
--source-map-target [string] Set `file` on returned source map.
--source-file-name [string] Set `sources[0]` on returned source map.
--source-root [filename] The root from which all sources are relative.
--module-root [filename] Optional prefix for the AMD module formatter that will be prepended to the filename on module definitions.
-M, --module-ids Insert an explicit id for modules.
--module-id [string] Specify a custom name for module ids.
-x, --extensions [extensions] List of extensions to compile when a directory has been the input. [.es6,.js,.es,.jsx,.mjs]
--keep-file-extension Preserve the file extensions of the input files.
-w, --watch Recompile files on changes.
--skip-initial-build Do not compile files before watching.
-o, --out-file [out] Compile all input files into a single file.
-d, --out-dir [out] Compile an input directory of modules into an output directory.
--relative Compile into an output directory relative to input directory or file. Requires --out-dir [out]
-D, --copy-files When compiling a directory copy over non-compilable files.
--include-dotfiles Include dotfiles when compiling and copying non-compilable files.
--no-copy-ignored Exclude ignored files when copying non-compilable files.
--verbose Log everything. This option conflicts with --quiet
--quiet Don't log anything. This option conflicts with --verbose
--delete-dir-on-start Delete the out directory before compilation.
--out-file-extension [string] Use a specific extension for the output files
-V, --version output the version number
-h, --help output usage information
위 plugins 옵션에 [list]를 넣어 전달하면 바벨로 플러그인을 실행할 수 있다.
npx babel app.js --plugins './my-babel-plugin.js'
위에 보시면 결과물은 아직은 원본 코드랑 같다.
대신 로그가 찍힌 것을 볼 수 있다.
// my-babel-plugin.js
module.exports = function myBabelPlugin() {
return {
visitor: {
Identifier(path) {
const name = path.node.name;
// 바벨이 만든 AST 노드를 출력한다.
console.log('Identifier() name:', name);
// 변환작업: 코드 문자열을 역순으로 변환한다.
path.node.name = name
.split("")
.reverse()
.join("");
}
}
}
}
주석처리했던 코드를 다시 풀자.
주석을 푼 코드는 실제로 변환작업을 하는 코드이다.
각 name
에 담긴 String
을 뒤집는 역할을 하는 코드이다.
로그로 찍힌 것들은 똑같고 결과물을 보면 문자가 뒤집혀진 것을 확인할 수 있다.
각 token
들이 역순으로 뒤집혀졌다.
우리가 여기서 하려는 것은 my-babel-plugin.js
파일이 app.js
코드를 받아서 ES6 문법을 ES5로 바꾸는 것이다.
그럼 첫번째로 const
라는 키워드를 var
라는 키워드로 바꾸는 플러그인을 만들어보도록 하겠다.
// my-babel-plugin.js
module.exports = function myBabelPlugin() {
return {
visitor: {
VariableDeclaration(path) {
console.log('VariableDeclaration() kind:', path.node.kind); // const
// const => var 변환
if (path.node.kind === 'const') {
path.node.kind = 'var'
}
}
}
}
}
npx babel app.js --plugins './my-babel-plugin.js'
visitor
객체는 남겨두고 Identifier
메서드는 지워버리자.
그리고 VariableDeclaration
메서드를 넣어주자.
VariableDeclaration
이것도 역시 path
객체를 넘겨받는다.
path.node.kind
에 가보면 const
값이 찍힐 것이다.
그래서 이 값이 만약 const
라면 var
라는 키워드로 바꾸는 작업을 하게된다.
Identifier
와는 다르게 VariableDeclaration
의 path
객체엔 const
같은 것이 들어온다.
함수(메서드)마다 다른 path
객체값이 들어온다.
플러그인 사용하기
이러한 결과를 만드는 것이 block-scoping 플러그인이다.
const
, let
처럼 블록 스코핑을 따르는 예약어를 함수 스코핑을 사용하는 var
로 변경한다.
방금 위에서 만들었던
const
를var
로 변경하는 플러그인이 실제 바벨에서도 제공하는 block-scoping 블러그인이다.
위 링크 들어가셔서 보시면 정확히 우리가 만들었던 플러그인과 동일한 기능을 한다는 것을 아실 수 있다.
그럼 우리가 만든 플러그인을 사용하지말고 바벨이 제공해주는 @babel/plugin-transform-block-scoping
이라는 패키지를 활용해서 변환작업을 해보자.
npm i -D @babel/plugin-transform-block-scoping
npx babel app.js --plugins @babel/plugin-transform-block-scoping
var
는 IE가 인식하지만 애로우 함수는 IE가 인식하지 못한다.
이번엔 애로우 함수를 변환하는 플러그인을 활용해보자.
npm i -D @babel/plugin-transform-arrow-functions
npx babel app.js --plugins @babel/plugin-transform-block-scoping --plugins @babel/plugin-transform-arrow-functions
위와 같이 const
와 =>
애로우 펑션 모두 변환된 것을 확인할 수 있다.
ECMAScript5에서부터 지원하는 엄격모드를 사용하는 것이 안전하기 때문에 "use strict"
구문을 추가해준다.
npm i -D @babel/plugin-transform-strict-mode
npx babel app.js --plugins @babel/plugin-transform-block-scoping --plugins @babel/plugin-transform-arrow-functions --plugins @babel/plugin-transform-strict-mode
그런데 이렇게 하다보니까 명령어가 점점 길어지고 있다.
바벨 플러그인이 많아지면 많아질수록 길어진다.
그래서 이는 설정 파일로 분리하는 것이 좋다.
웹팩 webpack.config.js
를 기본 설정파일로 사용하듯 바벨도 babel.config.js
를 사용한다.
// babel.config.js
module.exports = {
plugins: [
"@babel/plugin-transform-block-scoping",
"@babel/plugin-transform-arrow-functions",
"@babel/plugin-transform-strict-mode"
]
}
아직은 노드에서 돌아가는 거기 때문에 module
키워드를 사용했다.
npx babel app.js
바벨이 babel.config.js
파일을 읽어서 config를 적용하고, 코드를 변환한 후에 결과를 출력한다.
4. 프리셋
그런데 ES6로 코딩할 때 필요한 플러그인을 일일이 설정하는 일은 무척 귀찮은 일이다.
코드한 줄 작성하는데도 세 개 플러그인 세팅을 했으니 말이다.
목적에 맞게 여러가지 플러그인을 세트로 모아놓은 것을 프리셋이라고 한다.
프리셋을 사용하기 전에 프리셋도 커스텀으로 만들어보자.
4.1 커스텀 프리셋
// my-babel-preset.js
module.exports = function myBabelPreset() {
return {
plugins: [
"@babel/plugin-transform-block-scoping",
"@babel/plugin-transform-arrow-functions",
"@babel/plugin-transform-strict-mode"
]
}
}
프리셋에서는 plugins
라는 배열을 갖고있는 객체를 반환한다.
플러그인을 모아놓은 세트 - 프리셋이라고 보면된다.
이 프리셋을 사용하려면 babel.config.js
파일을 다음과 같이 수정해줘야된다.
// babel.config.js
module.exports = {
presets: [
'./my-babel-preset.js'
]
}
npx babel app.js
정리
커스텀 프리셋까지 했는데, 영상에서 다루었던 바벨 사용법은 실업무에선 잘 쓰이지 않는다.
이렇게 공부한 이유는 바벨이 어떤식으로 동작하는지 동작 원리를 이해하기 위해서 사용한 것이다.
이 다음엔 실제 실무에서 많이 사용하는 프리셋을 알아보고, 그리고 폴리필을 어떻게 추가하는지도 보고 웹팩으로 어떻게 통합할 수 있는지 한번 보도록 하겠다.