하루에 한 문제

바벨 (babel) 본문

Dev/JavaScript

바벨 (babel)

dkwjdi 2021. 4. 28. 01:27

바벨탑에 대해 알고 있는가? 바벨탑의 실질적 실패요인이 바로 언어의 혼란 때문이었다.

이렇게 언어의 혼란은 실패를 야기한다.

브라우저마다 사용하는 언어가 다르기 떄문에 프론트엔드 코드가 일관적이지 못할 때가 많다.

스펙가 브라우저가 개선되고 있지만 여전 IE나 Safari같은 브라우저는 지원하지 않는것이 많다.

프론트엔드 개발에서 크로스브라우징이슈는 코드의 일관성을 해친다

히브리어로 바벨이 '혼돈'이라는 뜻으로 쓰이는 것처럼 말이다.

 

크로스브라우징의 혼란을 해결해 줄 수 있는 것이 바로 '바벨'이다. ECMAScript2015+로 작성된 코드를 모든 브라우저에서 동작할 수 있도록 호환성을 지켜준다. 

 

바벨의 기본 동작

바벨은 ECMAScript2015 이상의 코드를 적당한 하위 버전으로 바꾸는 것이 주된 역할이다.

이렇게 바뀐 코드는 IE 나 구버전 브라우저처럼 최신 자바스크립트 코드를 이해하지 못하는 환경에서도 잘 동작한다.

 

바벨을 이용해 아래 코드를 인터넷 익스플로러가 이해할 수 있는 코드로 바꿔 보겠다.

app.js

const alert = msg => window.alert(msg)

먼저 바벨 최신 버전를 설치한다. 터미널 도구를 사용하기 위해 커맨드라인 도구도 함께 설치하자.

npm install -D @babel/core  @babel/cli

설치를 완료후 node_modules/.bin 폴더에 추가된 바벨 명령어를 사용할 수 있다.

결과를 확인해 봤는데 이전 코드와 다른게 하나도 없다....?

 

바벨을 총 세 단계로 빌드를 진행한다.

1. 파싱(Parsing)

2. 변환(Transforming)

3. 출력(Printing)

 

우선 코드를 읽고 추상 구문 트리(AST)로 변환하는 단계를 "파싱"이라고 한다.

추상 구문 트리를 변경하는 것이 "변환"단계이다. 즉 실제로 코드변경 작업을 한다.

변경된 결과물을 "출력"하는 것을 마지막으`로 바벨은 작업을 완료한다.

 

플러그인이 없기 때문에 변환작업이 일어나지 않은 것이다. 

 

 

커스텀 플러그인

플러그인을 직접 만들면서 동작의 원리를 살펴보자. myplugin.js라는 파일을 만들어보자.

// myplugin.js:
module.exports = function myplugin() {
  return {
    visitor: {
      Identifier(path) {
        const name = path.node.name

        // 바벨이 만든 AST 노드를 출력한다
        console.log("Identifier() name:", name)

        // 변환작업: 코드 문자열을 역순으로 변환한다
        path.node.name = name.split("").reverse().join("")
      },
    },
  }
}

플러그인 형식은 visitor 객체를 가진 함수를 반환해야 한다.

이 객체는 바벨이 파싱하여 만든 추상 구문 트리(AST)에 접근할 수 있는 메소드를 제공한다.

그중 Identifier() 메소드의 동작 원리를 살펴보는 코드다.

 

우선 플러그인 사용법을 알아보자.

npx babel --help

--plugins 옵션에 플러그인을 추가하면 된다.

Identifier() 메소드로 들어온 인자 path에 접근하면 코드 조각에 접근할 수 있는 것 같다.

path.node.name의 값을 변경하는데 문자를 뒤집는 코드다.

결과의 마지막 줄에서 보는것 처럼 이 코드의 문자열 순서가 역전되었다.

결과물을 확인하면 코드의 문자열이 역순으로 바뀐것을 볼 수 있다.

 

우리가 하려는것은 ECMASCript2015로 작성한 코드를 인터넷 익스플로러에서 돌리는 것이다.

먼저 const 코드를 var로 변경하는 플러그인을 만들어 보겠다.

// myplugin.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"
            }
          },
      },
    }
  }

이번에는 vistor 객체에 VariableDeclaration() 메소드를 정의했다.

path에 접근해 보면 키워드가 잡히는 걸 알 수 있다.

path.node.kind가 const 일 경우 var로 변환하는 코드다.

이 플러그인으로 다시 빌드해보자.

마지막줄에 보면 const코드가 var로 변경되었다.

 

플러그인 사용하기

이러한 결과를 만드는 것이 block-scoping 플러그인이다.

const, let 처럼 블록 스코핑을 따르는 예약어를 함수 스코핑을 사용하는 var로 변경한다!

 

우선 플러그인을 설치하자

npm install -D @babel/plugin-transform-block-scoping

그리고 실행을 해보면 아래와 같은 결과를 확인할 수 있다!

IE는 화살표 함수도 지원하지 않는데 arrow-functions 플러그인을 이용해서 일반 함수로 변경할 수 있다.

 

In

Out

 

우선 마찬가지로 설치를 해보자.

npm install -D @babel/plugin-transform-arrow-functions

실행을 해보면 아래와 같이 arrow-function이 변환된것을 확인할 수 있다.

 

ECMAScript5에서부터 지원하는 엄격 모드를 사용하는 것이 안전하기 때문에 "use strict" 구문을 추가해야 겠다.

strict-mode 플러그인을 사용하자.

 

역시 설치를 먼저 해보자

npm install @babel/plugin-transform-strict-mode

실행을 하면 제일 상위에 "use strict"가 선언된 것을 볼 수 있다!

그런데 현재 명령어를 보자 딱 봐도 매우길다... 설정 파일로 분리를 한번 해보자

웹팩에서 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",
    ],
}

그리고 다시 빌드해보자 npx babel app.js 를 입력 했을 뿐인데 위의 결과와 동일한 결과를 얻을 수 있다.

 

 

프리셋

ECMAScript2015+으로 코딩할 때 필요한 플러그인을 일일이 설정하는 일은 무척 지난한 일이다. 코드 한 줄 작성하는데도 세 개 플러그인 세팅을 했으니 말이다. 목적에 맞게 여러가지 플러그인을 세트로 모아놓은 것을 "프리셋"이라고 한다.

 

커스텀 프리셋

사용한 세 개 플러그인을 하나의 프리셋으로 만들어보자. mypreset.js파일을 다음과 같이 작성해보자.

// mypreset.js
module.exports = function mypreset() {
    return {
      plugins: [
        "@babel/plugin-transform-arrow-functions",
        "@babel/plugin-transform-block-scoping",
        "@babel/plugin-transform-strict-mode",
      ],
    }
  }

plugins 배열에 사용한 세 개 플러그인을 담았다.

프리셋을 사용하기 위해 바벨 설정을 약간 수정한다.

module.exports = {
    presets : ["./my-babel-preset.js"],
}

플러그인 세팅 코드를 제거하고 presets에 방금 만든 mypreset.js를 추가했다. 실행해보면 동일한 결과를 출력할 것이다.

 

프리셋 사용해보기

바벨은 목적에 따라 몇 가지 프리셋들을 제공한다.

preset-env

preset-flow

preset-react

preset-vue

preset-typescript

 

preset-env는 ECMAScript2015+를 변환할 때 사용한다. 

 

우선 preset-env를 사용해보자.

우선 설치를 해야한다.

npm install -D @babel/preset-env

설치한 바벨 설정을 바꿔준다.

module.exports = {
    presets : ['@babel/preset-env'],
}

그리고 빌드를해보면!

위에서 만든 mypreset.js와 같은 결과를 출력하는 것을 확인할 수 있다.

 

타겟 브라우저

우리 코드가 크롬 최신버전만 지원한다고 해보자.

그렇다면 IE를 위한 코드 변환은 불필요하다.

target 옵션에 브라우저 버전명만 지원하면 env프리셋은 이에 맞는 플러그인들을 찾아 최적의 코드를 출력해 낸다.

module.exports = {
    presets: [
        [
            '@babel/preset-env', {
                targets: {
                    chrome: '79'
                }
            }
        ]
    ],
}

그리고 실행해보면 아까와른 결과를 볼 수 있다.

아까와는 다르게 엄격모드는 있지만 arrow funtion과 const는 그대로 나오는 것을 볼 수 있다.

 

크롬은 블록 스코핑과 arrow funtion을 지원하기 때문에 코드를 변환하지 않고 이러한 결과물을 만들었다. 만약 IE도 지원해야 한다면 바벨 설정에 브라우져 정보만 하나 더 추가하면 된다.

module.exports = {
    presets: [
        [
            '@babel/preset-env', {
                targets: {
                    chrome: '79',
                    ie : "11",
                }
            }
        ]
    ],
}

 

 

폴리필

이번엔 변환과는 조금 다른 폴리필에 대해 알아보자.

ECMAScript2015의 Promise객체를 사용하는 코드를 만들어보자.

new Promise();

바벨로 처리한다면 어떻게 될까??

env프리셋을 사용했지만 Promise는 변함이 없다.... 분명 target에 ie 11을 설정했는데 

왜일까??? IE는 Promise를 지원하지 않기 때문에 Promise를 해석하지 못하고 에러를 준다..

 브라우저는 현재 스코프부터 시작해 전역스코프까지 Promise라는 이름을 찾으려고 할것이다.

하지만 스코프 어디에도 Promise가 없기 때문에 레퍼런스 에러를 던지고 프로그램은 죽는다.

 

플러그인이 Promise를 ECMAScript5버전으로 변환해줄거라고 생각했는데 그러지 못했다..

바벨은 ECMAScript2015+를 ECMAScript5버전으로 변환할 수 있는 것만 빌드한다.

그렇지 못한 것들은 "폴리필"이라고 부르는 코드조각을 추가해서 해결한다.

 

만약 ECMAScript2015의 블록 스코핑은 ECMAScript5의 함수 스코핑으로 대체할 수 있다.

화살표 함수도 일반 함수로 대체할 수 있다.

이러한 것들은 바벨이 변환해서 ECMAScript5버전으로 결과물을 만들어준다.

 

한편 Promise ECMAScript5 버전으로 대체할 수 없다. 다만 ECMAScript5버전으로 구현은 가능하다!

env프리셋은 폴리필을 지정할 수 있는 옵션을 제공한다!

module.exports = {
    presets: [
        [
            '@babel/preset-env', {
                targets: {
                    chrome: '79',
                    ie : "11",
                },
                useBuiltIns: 'usage',
                corejs: {
                    version : 2, 
                }
            }
        ]
    ],
}

useBuiltIns는 어떤 방식으로 폴리필을 사용할지 설정하는 옵션이다. (usage, entry, false)

기본값은 false 이고 usage, entry 를 설정하면 폴리필 패키지 중 core-js를 모듈로 가져온다.

 

폴리필이 추가된 결과물을 확인해 보자!

음 일단 Promise는 그대로 있다.

그런데 그 위에 core-js패키지로부터 프라미스 모듈을 가져오는 임포트 구문이 상단에 추가되었다.

이제야 비로서 IE에서도 돌아가는 결과물을 만들게 되었다!

 

웹팩으로 통합

실무 환경에서는 바벨을 직접 사용하기보다는 웹팩으로 통합해서 사용하는 것이 일반적이라고 한다.

이를 로더 형태로 제공하는데 babel-loader가 그것이다!

 

우선 패키지를 설치하자

npm install -D babel-loader

웹팩 설정에 로더를 추가해주자

// webpack.config.js:
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader", // 바벨 로더를 추가한다
      },
    ],
  },
}

.js 확장자로 끝나는 파일은 babel-loader가 처리하도록 설정했다.

사용하는 써드파티 라이브라리가 많을수록 바벨 로더가 느리게 동작할 수 있는데 node_modules 폴더를 로더가 처리하지 않도록 예외 처리했다

 

폴리필 사용 설정을 했다면 core-js도 설치해야한다.

웹팩은 바벨 로더가 만든 아래 코드를 만나면 core-js를 찾을 것이기 때문이다.

버전 2로 패키지를 추가하자.

npm i core-js@2

그리고 웹팩을 이용해 빌드를 해보자!

그러면 dist폴더에 main.js가 생겼을것이다! 그리고 그 main.js안에는 이런식으로 설정이 되어 있을 것이다!

 

 

 

바벨은 일관적인 방식으로 코딩하면서, 다양한 브라우져에서 돌아가는 어플리케이션을 만들기 위한 도구다.

바벨의 코어는 파싱과 출력만 담당하고 변환 작업은 플러그인이 처리한다.

여러 개의 플러그인들을 모아놓은 세트를 프리셋이라고 하는데 ECMAScript+ 환경은 env 프리셋을 사용한다.

바벨이 변환하지 못하는 코드는 폴리필이라 부르는 코드조각을 불러와 결과물에 로딩해서 해결한다.

babel-loader로 웹팩과 함께 사용하면 훨씬 단순하고 자동화된 프론트엔드 개발환경을 갖출 수 있다.

 

 

 

참고

jeonghwan-kim.github.io/series/2019/12/22/frontend-dev-env-babel.html

babeljs.io/docs/en/babel-plugin-transform-arrow-functions

babeljs.io/docs/en/babel-plugin-transform-block-scoping

'Dev > JavaScript' 카테고리의 다른 글

template literal (ECMAScript2015)  (0) 2021.04.28
스코프 (ECMAScript 2015 )  (0) 2021.04.28
웹팩 플러그인  (0) 2021.04.27
웹팩 로더  (0) 2021.04.27
프론트엔드 개발에 Node.js가 필요한 이유  (1) 2021.04.26
Comments