imch.devabout

Babel 기초 다지기

CRA로 프로젝트를 진행했기 때문에 바벨 설정을 생략할 수 있었지만, 자바스크립트를 주 언어로 사용하면서 바벨을 모른척 할 수는 없기에 이번 포스팅에서는 처음부터 바벨을 설치해서 적용하는 방법을 정리하기로 했다.

바벨이란 무엇일까?

바벨은 ES6+ 이상의 최신 ECMAScript 문법을 이전 자바스크립트 문법(~ES5)으로 변환 해주는 컴파일러다.

트랜스파일러가 더 정확하지 않을까 생각했는데, 바벨 홈페이지에 들어가보면 Babel is a JavaScript compiler라고 쓰여있다.

그렇다면 왜 컴파일러일까? 내 생각은 실행 환경에 따라 자바스크립트를 실행할 수 없기 때문이라고 생각한다. 이는 자연스럽게 바벨이 왜 필요한지 생각해 보게 된다.

(추가) 트랜스파일러는 소스를 입력받으면 다시 다른 소스로 반환하는, 컴파일러의 일종이기 때문에 컴파일러라고 부르는 것은 문제가 되지 않는다. 정확하게 알고 있지 않은 내용을 이해하기 쉽게 가공하는 것은 오류를 범할 수 있다. 올바르게 지적해 주신 생활코딩의 장준영님께 감사드립니다.

바벨이 왜 필요할까?

브라우저 호환성 표에 따르면, ES2015 문법은 대부분의 브라우저가 평균 98% 정도로 지원하고, 최근 버전의 크롬 브라우저는 ES2016+ 문법을 제법 지원한다. 크로미움 기반 엣지와 크롬 79버전 기준 무려 **92%**에 달한다!

익스플로러를 지원할 계획이 없는 프로젝트를 ES2015 문법으로만 작성한다면, 이론상 바벨이 필요없는 수준인 것이다.

그럼에도 바벨이 필요한 이유는 1. 사실상 익스플로러를 고려해야 하고 2. 사용하고 싶은 최신 ECMAScript 문법을 아직 지원하지 않는 모던 브라우저 때문에 필요한 것이다.

이런 측면에서 바벨은 최대한 모든 실행 환경에서 자바스크립트가 실행될 수 있도록 보장해주기 때문에 컴파일러라고 해도 손색이 없다고 생각한다.

바벨에 대한 오해

내가 했던, 지극히 주관적인 오해들을 정리했다.

첫째, 바벨 그 자체가 많은 것을 한다?

바벨은 컴파일(트랜스파일)만 담당한다.

바벨을 사용하면 컴파일과 동시에 여러 작업이 일어나는 것으로 착각했다. 바벨이 웹팩과 같은 번들러와 아주 밀접한 관계에 있기 때문에 바벨과 웹팩 사이의 경계를 눈치채지 못했기 때문이다.

둘째, 바벨 하나만 설치하면 된다?

바벨 코어 자체는 최신 ECMAScript를 해석할 줄 모른다.

정확하게 짚고 넘어가면, 컴파일하는 일련의 작업을 바벨 코어(@babel/core)가 전담 하는 것이 아니다. 바벨 코어는 자바스크립트 코드를 분석하고 바벨 설정 파일(.babelrc.json)에 따라 컴파일한다.

코드를 어떻게 변환 하는지는 프리셋(presets)이나 플러그인(plugins)이 알고 있다.

셋째, 바벨은 설치할게 너무 많다.

바벨이 모노레포인 점은 고사하더라도, 바벨에 익숙하지 않은 사람들은 주로 아래와 같은 글의 일부를 봤을 것이다.

yarn add -D @babel/core @babel/cli \
              @babel/register @babel/preset-env \
              @babel/preset-foo @babel/plugin-bar
              @babel/why-should-i-install-this
              # ...

이 패키지들을 그대로 붙여넣은 뒤, 설치는 하긴 하는데 왜 설치하는지 모르는 것이 원인이었다.

그래서 요리에 비유를 해보자면 다음과 같다.

  • @babel/core 코어 패키지는 요리사다. 재료와 레시피가 있다면 요리를 만들어낼 수 있는 기본기가 있다.
  • @babel/cli CLI 패키지는 조리 도구다. 간단한건 손으로도 할 수도 있지만 복잡한 요리를 편하게 하려면 조리 도구가 필요하다.
  • 플러그인 플러그인은 레시피이다. 재료를 손질하거나 요리하는 방법이 정리되어 있다.
  • 프리셋 프리셋은 레시피 북이다. 요리를 만들 때 참고할 수 있도록 여러 레시피를 모아둔 책이다. 참고로 최근에 가장 인기있는 레시피 북은 @babel/preset-env다.

정리하자면, 기본적인 바벨 기능을 사용하려면 코어 패키지와 CLI 패키지를 설치하고, 필요한 플러그인이나 프리셋을 사용하면 된다. 여기에 웹팩과 같은 번들러를 사용하기 위해 로더가 필요한 것이지, 필수로 설치해야 하는 것이 아니다.

시작하기

이제 바벨 패키지들을 하나씩 설치하면서 어떻게 작동하는지 알아보자.

@babel/core 설치

yarn add -D @babel/core

@babel/core는 로컬 설정 파일을 바탕으로 코드를 변환하는 API만 제공한다. 즉, @babel/core를 설치하면 아래와 같이 사용할 수 있다.

import * as babel from '@babel/core';

babel.transform(code, options, (err, result) => {
  console.log(result); // => { code, map, ast }
});

@babel/cli 설치

API 방식 말고, 터미널에서 babel 명령어를 입력하여 사용하기 위해서는 @babel/cli 패키지를 설치해야 한다.

yarn add -D @babel/cli

그리고 샘플 코드를 준비한다.

// sample.js
const arr = [1, 2, 3, 4, 5];

arr.map(value => value ** 2).forEach(value => console.log(value));

이제 터미널에서 명령어를 입력하여 컴파일을 시도 해보자.

yarn babel sample.js

실행 결과는 아래와 같다.

'use strict';

const arr = [1, 2, 3, 4, 5];
arr.map(value => value ** 2).forEach(value => console.log(value));

맨 위에 use strict가 추가된 것을 제외하면 ES5 문법으로 변경되지 않은 것을 볼 수 있다.

@babel/preset-env 설치

위에서 플러그인과 프리셋을 설명할 때 각각 레시피와 레시피 북으로 비유했다. 새로운 문법이나 기능에 대한 플러그인을 전부 나열할 수도 있지만 편의를 위해 프리셋이 제공된다고 이해할 수 있다.

예전에는 babel-preset-es2015, babel-preset-es2016과 같은 식으로 ECMAScript 버전에 해당하는 프리셋이 있었는데, 나중에 babel-preset-latest로 통합되었다가 Deprecated 되었고 babel-preset-env로 변경되었다.

이후 패키지 이름의 문제를 해결하기 위해 바벨 7버전 이후부터 @babel 스코프를 사용하기 시작하여 지금의 @babel/preset-env가 되었다. 따라서 2020년에 바벨을 학습할 때 최신 버전의 코어, CLI, 프리셋 등을 사용할 수 있도록 주의해야 한다.

이제 가장 많이 사용하는 프리셋을 설치해보자.

yarn add -D @babel/preset-env

설치한 프리셋은 —presets 옵션으로 사용할 수 있다.

yarn babel sample.js --presets=@babel/preset-env

실행 결과는 아래와 같다. (마크다운에 프리티어가 적용되어 줄바꿈이나 따옴표 등, 실제 출력과 약간의 차이는 있다.)

'use strict';

var arr = [1, 2, 3, 4, 5];
arr
  .map(function(value) {
    return value * 2;
  })
  .forEach(function(value) {
    return console.log(value);
  });

프리셋을 사용한 결과를 확인한 결과, ES5 문법에 맞게 변환된 것을 볼 수 있다. 또한, 실제로 바벨이 처리하는 것이 아닌, 플러그인과 프리셋이 실질적으로 변환하는 방법을 알고 있다는 뜻을 이해할 수 있게 되었다.

바벨이 프리셋 패키지를 찾는 방법

먼저 아래와 같이 @babel/env로 입력하여, preset-이 빠진 이름으로 바벨을 실행해보자.

yarn babel sample.js --presets=@babel/env

결과가 어떻게 나올까? 우리가 설치한 것은 @babel/preset-env인데 @babel/env로 입력하면 실행이 될까? 예상과 다르게 @babel/preset-env을 사용한 실행 결과와 동등한 출력을 확인할 수 있다.

바벨은 —presets 옵션으로 전달되는 값에 프리셋과 관련된 접두사가 붙어야 한다고 예상하기 때문에, 바벨이 실행될 때 전달되는 값에 대한 이름 정규화 과정을 진행한다. 몇가지 규칙 중 기본적으로 알아 두어야 할 규칙을 소개하자면,

  1. 기존에 사용한대로 접두사를 붙여서 사용하면 별도의 정규화를 거치지 않는다.
  2. 별도의 접두사를 붙이지 않는 경우, babel-preset-을 붙여서 패키지를 찾는다. —presets=env는 babel-preset-env를 사용한다.
  3. @babel 스코프를 사용하는 경우, 스코프와 이름 사이에 preset-을 붙여서 패키지를 찾는다. —presets=@babel/env는 @babel/preset-env를 사용한다.
  4. 바벨이 아닌 다른 스코프, 예를 들어 @scope를 단독으로 사용하면, /babel-preset을 붙여서 패키지를 찾는다. —presets=@scope는 @scope/babel-preset을 사용한다.
  5. 바벨이 아닌 다른 스코프, 예를 들어 @scope/foo를 사용하면, 스코프와 이름 사이에 babel-preset-을 붙여서 패키지를 찾는다. —presets=@scope/foo는 @scope/babel-preset-foo를 사용한다.

위 규칙은 플러그인에도 동일하게 적용된다.

바벨의 이름 정규화 규칙을 알아야 하는 이유는 나중에 다른 보일러플레이트나 강의 글을 볼 때 _“왜 @babel/env라고 사용하는거지? 설치한 건 @babel/preset-env가 아니였나?”_와 같은 혼란을 방지할 수 있다.

이외에도 절대 경로 등 더 많은 규칙이 존재한다. 자세한 내용은 바벨 공식 문서의 Name Normalization 섹션을 참조하자.

.babelrc.json

매번 —presets 옵션을 주고 터미널에서 실행하는 것은 번거롭기 때문에 설정 파일을 추가하자. 이 설정 파일은 바벨 코어가 자동으로 읽어서 사용하게 된다.

touch .babelrc.json

생성한 .babelrc.json 파일에 아래와 같이 입력하고 저장한다.

{
  "presets": ["@babel/env"]
}

이제 —presets 옵션을 빼고 바벨을 실행해도 @babel/preset-env 프리셋을 사용한 결과물이 나타나게 된다. 나중에 웹팩을 구성할 때에도 (웹팩 설정에 직접 전달할 수도 있지만) 유용하게 사용될 것이다.

.babelrc vs .babelrc.json

바벨 7 버전부터 모노레포에 대한 지원이 강화되었다.

바벨을 모노레포 프로젝트에서 사용할 때 몇가지 문제가 있었는데, 예를 들면 의도치 않게 node_modules 내부의 패키지에도 적용이 되거나, 반대로 심링크로 연결한 패키지에 의도한대로 적용이 되지 않거나, 실제로 서브 패키지에 설치하지 않은 프리셋이나 플러그인을 사용하여 유효하지 않은 설정도 작동이 되는 문제가 있었다.

이를 해결하기 위해 프로젝트 레벨의 .babel.config.json과 서브 패키지의 .babelrc.json을 사용할 수 있도록 변경되었다.

기초를 다지는 과정에서 이 내용은 무조건 알아야 하는 지식은 아니기 때문에 이해가 되지 않아도 넘어가도 괜찮을 것 같다.

요점은 .babelrc.json 처럼 사용하는 것이 가장 최신 버전에서 지원하는 방법이고, 기존의 .babelrc는 하위 호환성을 위해 .babelrc.json의 축약형으로 제공된다.


이외에도 @babel/polyfill이 deprecated 되었다는 것과, browserslist 관련해서 더 찾아보는 것을 추천한다. 아직 내가 정확히 소화하지 못한 지식을 남기는 것은 좋지 않다고 판단되어 이 포스팅에서는 포함하지 않았다.

대신 아래 두 링크를 남긴다. 최민호님이 벨로그에 작성 해 주신 글이다.

이찬희

프론트엔드 엔지니어 이찬희 입니다. 리액트와 타입스크립트를 주로 사용합니다. 현재 당근에서 재직중입니다.

© 2023 iamchanii