바벨.js

babel.js

Babel is a JavaScript compiler.

바벨 공식 홈페이지에서 바벨을 소개하는 문구입니다. 저 문구처럼 바벨은 상위버전에 자바스크립트(이하 ECMAscript)를 하위버전으로 컴파일링 혹은 트랜스 파일링 해주는 도구입니다.

자바스크립트는 리빙 스탠다드로 매년 새로운 표준이 나오며, 신규 문법들이 추가되고 있습니다. 일반적으로는 ES6이전과 이후로 크게 나뉘며, 우리가 하는 대부분의 개발은 ES6이상의 문법을 활용하여 개발합니다. 하지만 우리가 개발한 코드가 그 어느곳에서나 동일하게 작동하리라고 보장하기는 어렵습니다. 가령, 옛 버전의 웹 브라우저를 사용중이라면 최신 버전의 문법을 해석하지 못하고 오류를 발생시킬 것입니다.

이런 문제를 해결하기 위해 우리는 바벨 js를 사용합니다. 또한 단순히 ECMA간의 버전관리나 jsx문법을 자바스크립트로 바꿔주는 등 다양한 일을 해줍니다.

바벨의 빌드 3단계

바벨은 자바스크립트 코드를 빌드하고, 출력합니다. 이 단계에서 3단계를 거치게 됩니다.

  1. 파싱 : 주어진 코드를 읽고 추상구문트리로 변환합니다.
  2. 변환 : 추상구문트리를 하위버전의 자바스크립트 코드로 변환합니다.
  3. 출력 : 변환한 코드를 출력합니다.

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

프로그래밍 언어로 작성된 소스 코드의 추상 구문 구조의 트리를 뜻합니다.

가령 다음 함수를 AST 형태로 변환하다고 가정하겠습니다.

function consolelog() {
  console.log('hi')
}

변환을 하게된다면 다음과 같이 변환할수 있습니다.

- Progrma {
    - body: [
        - FunctionDeclaration {
            - id:Identifier = $node {
                name:'consolelog';
            },
            expression: false,
            generator: false,
            async: false,
            params: [],
            - body:BlockStatement {
                - body : [
                    ...
                ]
            }
        }
    ]
    sourceType: "module"
}

차례대로 해석해보면,

  • 프로그램이 있다.
  • 내부에 body 가 있다.
  • body 내부에는 함수 정의가 있다.
  • 정의된 함수의 ID 는 consolelog 이다.
  • 이 함수는 expression 이 아니다.
  • 이 함수는 generator 가 아니다.
  • 이 함수는 async 가 아니다.
  • 파라미터가 없다.
  • Body 에는 BlockStatement 가 있다.

이렇듯 컴파일러는 우리가 작성한 소스코드에서 위와 같은 계층 구조로 된 정보를 추출한다고 합니다.

AST 를 생성하는데는 렉시컬 분석과 신택스 분석이 중요합니다.

렉시컬 분석 : 코드의 문자를 읽어 정해진 규칙에 따라 이들을 토큰으로 만들어 합치고, 이 과정에서 공백, 주석등은 삭제합니다. 렉시컬 분석기는 소스 코드를 글자 단위로 읽으며 공백, 연산자 기호, 특수 기호 등을 만나면 해당 단어가 끝났다고 인지하고 하나의 토큰으로 만든다.

신택스 분석 : 결과로 나온 토큰 목록을 트리 구조로 만들고 구조적, 언어적으로 문제가 있다면 에러를 내뱉습니다. 트리 구조를 만들며 불필요한 토큰 (중복된 괄호 등) 은 생략하며, 결과로 Abstract Syntax Tree 가 만들어진다.

참고로 추상 구문 트리는 컴파일러에 널리 사용되는 자료 구조인데, 이는 프로그램 코드의 구조를 표현하는 프로퍼티이기 때문이라고 합니다.

플러그인과 프리셋

바벨빌드 과정중 변환과정은 플러그인을 통해 변환되게 되고, 다양한 플러그인들을 묶어 사용하는것을 프리셋이라고 칭합니다.

다음은 바벨에서 자주 사용되는 프리셋들입니다.

  • preset-env : ES6 => ES5 변환할 때 사용
  • preset-react : react를 변환할 때 사용
  • preset-typescript : typescript를 변환할 때 사용
  • preset-flow : flow를 변환할 때 사용

이렇게 플러그인과 프리셋이 있다면, 단순하게 바벨을 실행시켜볼수 있습니다.

가령 다음과 같은 코드를 바벨을 통해 컴파일링 한다고 생각해봅시다.

const element = <div>babel test</div>
const text = `element type is ${element.type}`
const add = (a, b) => a + b

컴파일링 된 코드는 다음과 같습니다.

// npx babel index.js --presets=@babel/preset-react --plugins=@babel/plugin-transform-template-literals,@babel/plugin-transform-arrow-functions

const element = /*#__PURE__*/ React.createElement('div', null, 'babel test')
const text = 'element type is '.concat(element.type)
const add = function(a, b) {
  return a + b
}

보시는것처럼 jsx문법은 React.createElement로 변경되었으며, 템플릿 리터럴도 단순하게 변경되고 화살표함수는 단순 펑션으로 변경되었습니다.

바벨 실행시에 사용될 프리셋이나 플러그인을 설정해주지 않으면 Unexpected token Error가 발생합니다.

다만 이렇게 긴 플러그인과 프리셋을 매번 사용하는건 매우 귀찮은 일이 될것이므로, 설정파일을 만들수 있습니다.

루트에 babel.config.js를 만들고 다음과 같이 작성합니다.

const presets = ['@babel/preset-react']
const plugins = [
  '@babel/plugin-transform-template-literals',
  '@babel/plugin-transform-arrow-functions',
]
module.exports = { presets, plugins }

그 이후에는 babel index.js 명령어 만으로 사전에 셋팅된 프리셋과 플러그인을 사용할 수 있습니다.

또한 npx babel src/code.js —out-dir dist out명령어를 통해 컴파일링 된 자바스크립트 파일을 내보낼수도 있습니다.

직접 만들어보는 플러그인

자 기본적으로 바벨에서 변환은 플러그인을 통해 이루어진다고 했습니다. 그렇다면 이 플러그인을 직접 만들수도 있을것입니다.

우리는 이 플러그인을 직접 만들어볼것입니다. 다음은 바벨 홈페이지의 예제코드를 참고해 만든 커스텀 플러그인입니다.

module.exports = function myplugin() {
  return {
    visitor: {
      VariableDeclaration(path) {
        console.log(path)
        if (path.node.kind === 'const') {
          path.node.kind = 'var'
        }
      },
    },
  }
}

플러그인 형식은 visitor 객체를 가진 함수를 반환해야 합니다. 이 객체는 바벨이 파싱하여 만든 추상구문트리에 접근할 수 있는 메소드를 제공합니다. 그중 Identifier() 메소드의 동작 원리를 살펴보는 코드다.

path를 콘솔에 찍어보면 추상화된 구문트리가 json형식으로 정말 많이 보여질것입니다. 그중에서 저희는 const 를 var로 바꾼것입니다.

폴리필

여태까지는 전부 컴파일 즉, 변환에 대한 얘기를 했습니다. 즉 상위 자바스크립트 문법을 하위 자바스크립트도 읽을수 있게끔 변환해 주는것이었습니다.

하지만 아예 문법자체가 없는 경우는 어떨까요? 단순히 기능이 발전된것이 아니라 신규 문법이 만들어졌다면 하위 자바스크립트에서는 아예 인지조차 하지 못하고 있는 그대로 컴파일 할 것입니다. 그리고 오류를 발생시킬것입니다.

그래서 polyfill을 사용합니다.

polyfill : 브라우저에서 지원하지 않는 코드를 사용가능한 코드 조각이나 플러그인(추가기능)을 의미함

사실 이전에 말씀드렸던 async/await도 폴리필의 일종입니다.

// 해당코드를 폴리필하면...
function a() {
  console.log('a')
}

async function b() {
  console.log('b1')
  await a()
  console.log('b2')
}
b()
console.log('c')

// 이렇게 바뀝니다...
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
  try {
    var info = gen[key](arg)
    var value = info.value
  } catch (error) {
    reject(error)
    return
  }
  if (info.done) {
    resolve(value)
  } else {
    Promise.resolve(value).then(_next, _throw)
  }
}

function _asyncToGenerator(fn) {
  return function() {
    var self = this,
      args = arguments
    return new Promise(function(resolve, reject) {
      var gen = fn.apply(self, args)
      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value)
      }
      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err)
      }
      _next(undefined)
    })
  }
}

function a() {
  console.log('a')
}

function b() {
  return _b.apply(this, arguments)
}

function _b() {
  _b = _asyncToGenerator(function*() {
    console.log('b1')
    yield a()
    console.log('b2')
  })
  return _b.apply(this, arguments)
}

b()
console.log('c')

이렇듯 우리는 상위 자바스크립트 문법이나 지원하지 않는 문법도 바벨의 컴파일링과 폴리필을 통해 다양한 크로스 브라우징 환경에서 모두가 동일한 웹을 사용할수 있도록 노력해야 할 것입니다.

이후 웹팩을 통해 빌드나, 번들링을 좀더 쉽게 할수 있는데 이는 다음에 알아보겠습니다.


Written by@JeongYeonJae
이것저것 쓰는 개발블로그

ResumeGitHub