Babel 컴파일을 살펴보자

sloth·2020년 11월 9일
3

Babel

Babel(바벨)이란?

  • Babel은 입력과 출력이 모두 자바스크립트 코드인 컴파일러이다.

    Babel is a JavaScript compiler

    Babel is a toolchain that is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in current and older browsers or environments. (중략) ...

    출처 : https://babeljs.io/docs/en/

  • 초기의 Babel은 주로 ES6+코드를 ES5 코드로 변환해주는 역할을 수행했다. 그 외에도 현재는

    • 리액트 JSX, Typescript, 코드 압축, Proposal 단계, ...

Babel을 실행하는 방법

  • @babel/core를 직접 사용하기

  • @babel/cli로 실행하기

  yarn add @babel/core @babel/cli @babel/preset-react 
  	@babel/plugin-transform-destructuring
  # 실행하기
  # 다수의 presets나 plugins는 ,(comma)를 구분자로 하여 쓰면 된다.
  npx babel src/before.js --presets=@babel/preset-react
  	--plugins=@babel/plugin-transform-destructuring --out-file dist/after.js
yarn add @babel-loader webpack webpack-cli
// webpack.config.js
module.exports = {
  entry: "./src/before.js",
  output: {
    filename: "after.js",
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-react"],
            plugins: ["@babel/plugin-transform-destructuring"],
          },
        },
      },
    ],
  },
  optimization: { minimizer: [] },
};
  • @babel/register로 실행하기

    ... The require hook will bind itself to node's require and automatically compile files on the fly. (중략)...

    출처: https://babeljs.io/docs/en/babel-register

    • 다른 방식들은 빌드하는 과정에서 babel을 실행해 컴파일 작업을 수행.
    • Node.js에서 동적으로 babel을 실행시켜 런타임에 파일을 컴파일하여 실행.

코드를 바꾸는 여정

위에서 언급한 방법들은 내부적으로 @babel/core를 사용해 컴파일을 진행합니다. 즉, 직접 @babel/core 모듈의 API를 사용해 직접 코드를 작성해 컴파일 작업을 수행할 수 있습니다. 실제 어떤 방식으로 @babel/core가 컴파일 작업을 수행하는지 하나씩 살펴보겠습니다.

babel은 크게 3단계를 거쳐서 컴파일 작업을 수행합니다.

source code --> AST --> transformed AST --> transformed code

  1. 파싱(parse) 단계
    • 입력 코드를 파싱하여 AST(Abstract syntax tree)를 생성합니다.
  2. 변환(transform) 단계
    • 1번 단계에서 만들어진 AST를 사용자가 원하는 형태로 AST를 변형합니다.
  3. 생성(generate) 단계
    • 변형된 AST를 바탕으로 최종 자바스크립트 코드를 생성합니다.

AST(Abstract Syntax Tree, 추상구문트리)

간단히 AST가 무엇인지 알아보고 가겠습니다. babel은 AST를 통해 코드를 조작하기 때문에 이를 알고 갈 필요가 있습니다.

In computer science, an abstract syntax tree (AST), or just syntax tree, is a tree representation of the abstract syntactic structure of source code written in a programming language. Each node of the tree denotes a construct occurring in the source code.

출처: https://en.wikipedia.org/wiki/Abstract_syntax_tree

간단히 말하면, AST는 코드의 구문 분석 결과를 트리 형태로 표현한 것입니다. 컴파일러는 AST를 통해 프로그램 코드 구조를 쉽게 파악할 수 있어, 문법 오류를 확인하거나 AST를 조작하여 최종 코드에 변형을 줄 수 있습니다.

자바스크립트 코드는 Estree스펙을 따라 AST로 표현되며, 이러한 AST를 만드는 대표적인 파서로 acorn, @babel/parser등이 있습니다. 다음은 예시 코드를 acorn으로 파싱한 AST를 JSON 형태로 나타난 결과입니다. (https://astexplorer.net/에서 AST변환을 해볼 수 있으니, 한번 해보시는 것도 좋을 것 같습니다.)

const name = "javascript";
{
  "type": "Program",
  "start": 0,
  "end": 26,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 26,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 25,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 10,
            "name": "name"
          },
          "init": {
            "type": "Literal",
            "start": 13,
            "end": 25,
            "value": "javascript",
            "raw": "\"javascript\""
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "module"
}

babel 맛보기

아까 위에서도 언급했듯이, 컴파일 과정은 크게 파싱, 변형, 생성 3단계로 나뉩니다. babel을 통해 3단계를 거쳐 코드를 변환하는 예제 코드를 한번 보겠습니다.

아래 예제는 소스코드 중 문자열 리터럴의 첫 알파벳을 대문자로 변형해주는 예제입니다.

파싱 단계

@babel/parser의 parse 함수를 통해 자바스크립트 코드를 AST로 바꿔보겠습니다.

import * as parser from "@babel/parser";

const code = "const greeting = 'hello, world!';";

// 1. code --> AST
const ast = parser.parse(code);

변형 단계

제일 중요한 단계인 변형 단계입니다. AST 조작을 통해 실질적인 코드의 변화를 줄 수 있습니다.

// ...
import traverse from "@babel/traverse";

// ...
// 2. transform the AST
traverse(ast, {
  enter(path) {
    if (path.isStringLiteral()) {
      const origin = path.node.value;
      const converted = origin.charAt(0).toUpperCase();
      path.node.value =
        origin.length <= 1 ? converted : converted.concat(origin.substring(1));
    }
  },
});

생성 단계

생성은 간단합니다. 변형된 ast와 원래 code를 바탕으로 새로운 코드를 만들어 냅니다.

// ...
import generate from "@babel/generator";

// ...
// 3. transformed AST --> generate code
const result = generate(ast, code);
// 출력: const greeting = "Hello, world!";
console.log(result.code);

전체 코드

import * as parser from "@babel/parser";
import traverse from "@babel/traverse";
import generate from "@babel/generator";

const code = "const greeting = 'hello, world!';";

// 1. code --> AST
const ast = parser.parse(code);

// 2. transform the AST
traverse(ast, {
  enter(path) {
    if (path.isStringLiteral()) {
      const origin = path.node.value;
      const converted = origin.charAt(0).toUpperCase();
      path.node.value =
        origin.length <= 1 ? converted : converted.concat(origin.substring(1));
    }
  },
});

// 3. transformed AST --> generate code
const result = generate(ast, code);
// 출력: const greeting = "Hello, world!";
console.log(result.code);

또 다른 방식

위 예제는 컴파일 3단계를 parse, traverse, generate 함수를 차례대로 호출하여 처리하였는데, 이미 babel에서는 이 3단계를 한꺼번에 처리할 수 있는 함수를 제공합니다. 바로 @babel/core 모듈의 transformSync 함수입니다.

이번 예제에서는 transformSync함수를 써서 실제 babel plugin 형식으로 만들어 보겠습니다.

import fs from "fs";
import path from "path";
import { transformSync } from "@babel/core";
import firstUppercasePlugin from "./first-uppercase-plugin";

const filepath = path.resolve(__dirname, "..", "code.js");
const code = fs.readFileSync(filepath, "utf-8");

// transformSync 함수로 파싱, 변형, 생성을 한꺼번에!
const result = transformSync(code, {
  plugins: [firstUppercasePlugin],
  configFile: false,
});

console.log(result.code);
// first-uppercase-plugin.js
const firstUppercasePlugin = () => {
  return {
    visitor: {
      StringLiteral(path) {
        const origin = path.node.value;
        const converted = origin.charAt(0).toUpperCase();
        path.node.value =
          origin.length <= 1
            ? converted
            : converted.concat(origin.substring(1));
      },
    },
  };
};
export default firstUppercasePlugin;

babel의 플러그인은 visitor(방문자)라는 속성을 지닌 객체를 반환하는 함수입니다. 방문자는 AST의 각 노드들을 순회하면서 특정 작업을 수행할 수 있습니다. 그러한 작업은 방문자 객체 메서드를 통해 이루어지며, 위 예제에서는 StringLiteral 타입의 노드를 방문할 경우, 문자열 리터럴의 첫 글자를 대문자로 바꾸는 작업을 수행합니다.

(방문자에 대해 더 다루고 싶지만, 이번 글에서는 시간 관계상 여기서 마치겠습니다.... 다음을 기약해주세요...)

마치며...

Babel이 무엇이고, 어떻게 실행하는지 알아보며 기본적인 개념을 알아보았습니다. 그리고 실제 babel이 어떤 단계를 거쳐 코드를 컴파일하는지 알아보기 위해 babel이 제공하는 함수들을 이용해 예제를 작성해봤습니다.

하지만 여태까지 알아본 내용은 그저 수박 겉핡기 수준 밖에 안됩니다. 실제 유용한 플러그인을 만드려면, (위에서 잠깐 언급한) visitor, path 등과 같은 개념들을 잘 알고 있어야 변환 작업을 능수능란하게 할 수 있습니다. 다음에는 이 개념들을 상세히 알아보고, 다양한 변환 예시를 정리해보겠습니다.

참고

0개의 댓글