Babel 직접 적용하며 이해하기

sy u·2022년 7월 20일
7

🤔 Babel이란?

바벨은 자바스크립트 컴파일러이다.

Babel is a JavaScript compiler
Babel 공식페이지

🔎 컴파일러란?

컴파일러는 특정 프로그래밍 언어로 작성된 코드를 다른 프로그래밍 언어나 컴퓨터 프로세서가 이해할 수 있는 기계어로 변환하는 프로그램이다.
즉, 인간이 사용할 수 있는 언어로 작성된 프로그램을 전체 코드에 대한 오류를 검사하고 기계어로 된 프로그램으로 출력하는 번역기이다. 컴파일 뒤 실행파일을 생성한다.

그런데!!! 자바스크립트는 컴파일러가 아닌 인터프리터로 동작한다.

🔎 인터프리터란?

인터프리터는 특정 프로그래밍 언어로 작성된 코드를 기계어로 변환하는 프로그램이다.
인간이 사용할 수 있는 언어로 작성된 프로그램을 실시간으로 읽어 한 줄씩 번역한다. 한 줄씩 읽을 때마다 해당 줄에 오류가 있는지 확인한 후, 그 줄을 기계코드로 변환한다. 또한 실행파일을 생성하고 저장하지 않아 메모리는 적게 사용되지만 코드가 실행될 때마다 실행파일을 생성하여 compiler 보다 속도가 느리다.

바벨은 ES5+ 코드를 자바스크립의 하위 호환 버전으로 변환하여 오래된 브라우저에서 실행하도록 변환하는 컴파일러이다. (트랜스파일링: 특정 언어로 작성된 코드를 다른 언어로 변환)

JavaScript에 컴파일러가 필요한 이유

모든 브라우저가 최신 문법, 기술(ES6) 을 지원하지 않기 때문에 구 기능(ES5)으로 변환하는 작업이 필요하다.

⚙️ Babel이 하는 일

Transform syntax (구문 변환)

트랜스파일링은 최신의 자바스크립트 문법을 오래된 브라우저가 이해할 수 있도록 오래된 문법으로 변환해 준다.

babel-polyfill을 통해서 폴리필 기능을 지원

폴리필은 오래된 브라우저에 네이티브로 지원하지 않는 사용자가 사용하는 메서드, 속성, API가 존재하지 않을 때 추가해 준다.

바벨은 최신 문법을 오래된 문법으로 변환해 주는 트랜스파일러 역할만 할 뿐 최신 함수를 사용할 수 있는 건 아니다.
폴리필은 프로그램이 처음 시작할 때, 지원하지 않는 기능들을 지원해 준다.

바벨은 컴파일 때 실행되고 폴리필은 런타임에 실행된다.

JSX and React

바벨은 JSX 문법을 변환한다.

🔎 JSX란?

JavaScript를 확장한 문법이다. (Javascript + xml)
템플릿 언어(HTML 내부에 변수 및 문법을 사용할 수 있는 언어)로도 보일 수 있지만 JavaScript 코드 내부에서 HTML 문법을 사용한 것이다.

JSX 문법을 변환해야 하는 이유

JSX는 JavaScript 코드이지만 브라우저에서 단독으로 실행될 수 없다.
JSX를 바로 실행하면 브라우저는 JSX 문법을 이해하지 못하기 때문에 JSX 코드는 브라우저가 이해할 수 있는 JavaScript 코드로 트랜스파일링 해야 한다.

바벨로 컴파일된 JSX는 React.createElement를 호출하여 리액트 엘리먼트를 반환한다. 이러한 이유 때문에 JSX 문법을 사용하는 컴포넌트 파일에는 React를 import해줘야 한다.

import React from 'react';

const MyTest({ children }) = {
  return <div>{children}</div>;
}

JSX가 트랜스파일링되어 변환된 내용은 이 페이지에서 확인할 수 있다.

Babel은 어떻게 동작할까?

1. Parsing (파싱)

바벨은 소스 코드를 분석하여 AST(Abstract Syntax Tree)로 변환한다.

AST를 간단하게 알아보자

AST는 소스 코드의 추상 구문 구조의 트리이다. 컴파일러에서 자료 구조로 사용되며 컴파일러의 구문 부석(parsing) 단계의 트리로 표현된 결과물이다. @babel/parser의 parse 함수를 사용하여 AWT로 파싱 할 수 있다.

https://astexplorer.net/ 입력한 예시가 AST로 변환된 결과를 확인할 수 있다.

2. Transformation (변환)

이전 단계에서 생성된 AST를 브라우저가 지원하는 오래된 문법으로 AST를 변경한다.

이 단계에서 plugin 또는 preset(plugin 배열)에 의해 관리되는데 이때 사용되는 plugin들은 @babel/traverse을 사용하여 AST를 순회한다.

즉, @babel/traverse을 사용하여 AST를 순회하며 각 AST 노드들은 브라우저가 지원하는 코드를 나타내는 새로운 노드들로 대체되고 새로운 AST로 변경된다.

3. Code Generation (코드 생성)

바벨은 새로운 AST를 바탕으로 @babel/generator를 통하여 새로운 코드를 생성한다.

Babel을 직접 적용하며 알아보기

위에서 알아본 바벨의 작동 방식을 @babel/core @babel/cli @babel/preset-env를 사용하여 직접 적용하면서 알아보자.

1. 필요한 패키지 설치하기

npm install --save-dev @babel/core @babel/cli @babel/preset-env
  • @babel/core
    바벨의 핵심 기능이 있는 모듈 (parsing, traverse, generator를 한꺼번에 처리할 수 있는 함수를 제공 => transformSync)
  • @babel/cli
    @babel/core에 대한 인터페이스 역할을 하며 개발 프로세스와 잘 통합되는 도구
  • @babel/preset-env
    모던 자바스크립트를 지원하기 위한 모든 플러그인의 집합

2. 전체 구성 파일 생성하기

프로젝트의 루트에 babel.config.json 파일을 생성한 후, 아래 예시 코드로 설정한다.
아래 코드는 Babel 공식 홈페이지를 참고하였습니다.

// ./src/babel.config.json

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "edge": "17",
          "firefox": "60",
          "chrome": "67",
          "safari": "11.1"
        },
        "useBuiltIns": "usage",
        "corejs": "3.6.5"
      }
    ]
  ]
}

3. 바벨을 사용할 수 있는 명령어 등록

아래 코드를 package.json에 적용한 후, npm run compile을 터미널에 입력한다면 src 폴더에 있는 모든 JavaScript 파일을 구문 분석(parse) 하고 우리가 설정한 변환을 적용하여 각 파일을 dist 폴더로 출력한다.

// package.json

{
	...
    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1",
      "compile": "babel src -d dist"
  	},
    ...
}
├── dist/ 컴파일된 소스 코드
│   └── index.js
└── src/ 원본 소스 코드
    └── index.js


하지만 이렇게 dist로 출력된 코드는 여전히 원본 코드와 동일하다. 아직 어떤 변환을 적용하라는 설정을 하지 않았기 때문이다.

4. plugin 사용하여 코드 변환 수행하기

plugin은 바벨에게 코드 변환을 어떻게 수행할지 방법을 지시하는 자바스크립트 프로그램이다.
만약 ES2015+ 구문을 ES5로 변환하고자 한다면 @babel/plugin-transform-arrow-functions 플러그인을 사용하여 적용할 수 있다.

npm install --save-dev @babel/plugin-transform-arrow-functions
// package.json

{
	...
    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1",
      "compile": "babel src -d dist --plugins=@babel/plugin-transform-arrow-functions"
  	},
    ...
}

npm run compile 명령어를 입력하게 되면 src 내의 모든 자바스크립트 코드의 모든 화살표 함수는 ES5에 호환되는 함수 식으로 변환된다.

// ./src/index.js
const fn = () => {
  console.log('test');
};

// ./dist/index.js
"use strict";

const fn = function fn() {
  console.log('test');
};

5. preset을 사용하여 플러그인 추가하기

preset은 미리 정해진 플러그인 집합이다. preset을 사용하면 코드에 존재하는 다른 ES2015+ 기능에 대한 플러그인을 하나씩 추가하지 않아도 된다.

// package.json

{
	...
    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1",
      "compile": "babel src -d dist --presets=@babel/env"
  	},
    ...
}

npm run compile 명령어를 입력하게 되면 src 내의 모든 자바스크립트 코드의 모던 자바스크립트 구문은 ES5에 호환되는 구문으로 변환된다.

// ./src/index.js
const test = 'test';

const fn = () => {
  console.log('test');
};

// ./dist/index.js
"use strict";

var test = 'test';

var fn = function fn() {
  console.log('test');
};

6. Configuration

preset은 옵션을 사용할 수 있는데 명령어를 통해 cli 옵션과 preset 옵션을 모두 전달하는 대신 설정 파일을 사용하여 옵션을 전달할 수 있다.

프로젝트 root 위치에 babel.config.json 파일을 생성한 후, 아래 코드를 설정한다.
env preset은 targets에 설정한 브라우저에서 사용할 수 없는 기능에 대한 변환 플러그인만 로드한다.

// babel.config.json

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "edge": "17",
          "firefox": "60",
          "chrome": "67",
          "safari": "11.1"
        }
      }
    ]
  ]
}

7. Polyfill

폴리필을 사용하는 방법은 세 가지가 존재한다.

  • @babel/polyfill 모듈 사용하기
    아래 명령어를 통해 사용할 수 있다.
    npm install --save @babel/polyfill
    --save 옵션을 사용하는 이유는 폴리필은 런타임에 필요한 기능을 주입하기 때문이다.
    @babel/polyfill 모듈을 사용하면 모든 폴리필을 가져온다. 즉, 불필요한 폴리필 또한 가져와져 번들 사이즈가 커질 수 있다. 현재 @babel/polyfill은 더 이상 사용되지 않고 있다.
  • core-js에서 필요한 폴리필 직접 import 하여 사용하기
    아래 명령어를 사용해 설치한다.
    npm install core-js
    필요한 폴리필을 코드 상단에 import하여 사용한다.
    import 'core-js/actual/array/from';
    import 'core-js/actual/array/group';
    import 'core-js/actual/set';
    import 'core-js/actual/promise';
    import 'core-js/actual/structured-clone';
    import 'core-js/actual/queue-microtask';
    core-js에서 필요한 폴리필을 사용하기 위해서는 어떤 폴리필이 필요한지 알고 있어야 한다. 또 직접 import 해야 하기 때문에 실수가 발생할 수 있다.
  • @babel/preset-env에서 제공하는 옵션 사용하기
    사용하는 기능과 target 브라우저에서 누락된 기능에 대한 변환 및 폴리필만 포함한다.

@babel/preset-env에서 제공하는 옵션 사용하여 폴리필 적용하기

"useBuiltIns": "usage"을 설정한다.
usage로 설정하면 target 대상 환경에 누락된 기능이 있는지 모든 코드를 검사한 후 필요한 폴리필만 포함한다.

// babel.config.json
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "edge": "17",
          "firefox": "60",
          "chrome": "67",
          "safari": "11.1"
        },
        "useBuiltIns": "usage"
      }
    ]
  ]
}

Config Files

babel.config.* 사용하여 프로젝트 전체 구성(Project-wide configuration) 하기

babel.config.* 파일은 .json, .js, .tftps, .tftps 등의 확장자를 가진다.

프로젝트 전체 구성을 위해 바벨은 root 폴더에서 babel.config. 파일을 검색한다.
configFile(: string | boolean) 값을 사용하면 기본 구성 파일(babel.config.
) 검색 동작을 재정의할 수 있다. configFile을 false로 설정하면 프로젝트 전체 구성을 비활성 할 수 있다.

.babelrc.* 사용하여 상대 파일(File-relative configuration) 구성하기

.babelrc.* 파일은 .json, .js, .cjs, .mjs 확장자를 가진다.
.babelrc 파일은 확장자가 없다.
package.son 파일은 babel 키를 사용한다.

바벨은 컴파일 중인 'filename'에서 시작하는 디렉터리 구조를 검색하여 지원되는 확장자를 사용하여 프로젝트의 단일 부분에만 적용되는 구성을 로드한다. 프로젝트 내 서드파티 라이브러리가 바벨에 의해 변환되지 않기를 원할 때 사용할 수 있다.

상대 파일 구성은 기본 구성 파일보다 더 높은 우선순위로 구분되며 검색된다.

상대 파일 구성은 프로젝트 전체 구성 위에 병합되기 때문에 전체 구성을 재정의하여 사용할 수 있다.

.js 🆚 .json

.js 와 .json 외에도 다른 확장자를 사용할 수 있지만 아마 이 두 가지 확장자를 많이 사용할 것이다.
그렇다면 이 두 개 중 어떤 확장자를 사용하면 좋을까?
.js 구성 파일은 구성 함수(Config Function API)를 전달할 함수를 export 할 수 있기 때문에 바벨 구성(config) api가 노출될 수 있다.

api 객체는 구성 파일(config-file) 특정 API와 함께 바벨 자체가 인덱스 모듈에서 노출하는 모든 것을 노출한다.

// .js 구성 파일
module.exports = function(api) {
  return {};
};

.js 구성은 캐싱이 어려워진다. 바벨은 컴파일될 때마다 구성 내에서 참조되는 플러그인과 preset 함수들을 다시 실행하는 것 때문에 구성 함수를 다시 실행하는 것을 피하려 한다. 따라서 .js 구성 파일을 사용하면 구성 함수 API에 대한 캐시 설정을 따로 해줘야 하기 때문에 복잡도가 높아진다.

.js 구성파일 사용 시, 캐시에 대한 부분은 제가 이해한 부분을 설명한 것 입니다. 혹시 잘못된 부분이 있다면 댓글 남겨주세요.
Config Function API

위와 같은 .js 구성 파일 사용 시, 따라오는 단점 때문에 .json 확장자를 사용하는 것이 더 선호된다.

NextJs에 추가 플러그인 적용하기

NextJs가 제공하는 것

NextJs는 next/babel prset을 포함하고 있다. 이 preset에는 React 어플리케이션과 서버사이드 코드를 컴파일하는데 필요한 모든 것이 포함되어 있다.

Next.js includes the next/babel preset to your app, which includes everything needed to compile React applications and server-side code.
NextJs 공식홈페이지

Babel config 커스텀하기

NextJs에서 제공하는 기본 바벨 구성을 프로젝트의 root에 .babelrc. 또는 babel.config. 파일을 생성하여 확장할 수도 있다. 이때 구성 파일에 preset에는 next/babel preset을 꼭 설정해 줘야 한다.

preset-env의 모듈 옵션은 false로 유지해야한다. 그렇지 않으면 웹 팩 코드 분할이 해제된다.

{
	"presets": ["next/babel", {
      "preset-env": false
    }],
	"plugins": [],
}

이후, 원하는 preset이나 plugin을 배열 안에 추가하면 된다.

NextJS 컴파일러

NextJS는 12부터 SWC를 사용하여 Rust로 작성된 컴파일러를 기본적으로 활성화한다. NextJS 컴파일러를 사용하면 바벨보다 더 빠르게 컴파일 할 수 있다. 하지만 기존 바벨 구성이 있거나 NextJs 컴파일러가 지원하지 않는 기능을 사용하고 있는 경우 바벨을 계속 사용해야 한다.

결론

이번 글을 작성하며 얼마나 바벨에 대해 무지했는지 알 수 있는 시간이었다.
심지어 내가 진행하고 있는 개인 프로젝트(NextJs 사용) root에 .babelrc.js와 babel.config.json이 동시에 존재하고 있어 수정했다...

아직 바벨에 대한 모든 이해를 한 것은 아니기 때문에 이 글은 계속해서 수정할 예정이다.

🗃️ 참고 블로그

https://babeljs.io/docs/en/
https://bravenamme.github.io/2020/02/12/what-is-babel/
https://dev.to/leonardomso/a-complete-react-boilerplate-tutorialfrom-zero-to-hero-jig
https://blog.logrocket.com/why-you-dont-need-babel/
https://dev.to/dchhitarka/introduction-to-programming-compiler-and-interpreter-23a4
https://velog.io/@chumil7432/import-React-from-react%EB%8A%94-%EC%99%9C-%EC%93%B0%EB%8A%94-%EA%B1%B8%EA%B9%8C
https://velog.io/@himprover/%EB%A7%A8%EB%82%A0-%ED%95%9C%EA%B7%80%EB%A1%9C-%EB%93%A3%EA%B3%A0-%ED%9D%98%EB%A6%AC%EB%8A%94-%EB%B0%94%EB%B2%A8Babel%EC%9D%B4-%EB%AD%98%EA%B9%8C
https://cyfyifanchen.com/babel-under-the-hood/
https://dev.to/nakabonne/take-a-walk-the-go-ast-e02
https://velog.io/@chumil7432/import-React-from-react%EB%8A%94-%EC%99%9C-%EC%93%B0%EB%8A%94-%EA%B1%B8%EA%B9%8C
https://yamoo9.gitbook.io/webpack/babel/babel-cli-configure
https://velog.io/@logqwerty/Babel-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EC%9D%84-%EC%82%B4%ED%8E%B4%EB%B3%B4%EC%9E%90
https://kschoi.github.io/cs/babel-config-js-vs-babelrc/
https://velog.io/@logqwerty/Babel-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EC%9D%84-%EC%82%B4%ED%8E%B4%EB%B3%B4%EC%9E%90

0개의 댓글