Babel

김동현·2022년 1월 21일
0
post-thumbnail

바벨의 배경

브라우저에서 사용하는 자바스크립트 언어가 조금씩 다릅니다. 프론트엔드 코드가 각 브라우저에 맞게 구현되기 때문에 코드가 일관적이지 않을 때가 많습니다. 예를 들어, 과거 사파리 브라우저는 Promise.prototype.finally 메소드를 사용할 수 없었습니다. 그래서 이 메서드에 대한 폴리필을 추가하여 해결을 하였었습니다. 이런 크로스 브라우징 이슈는 코드의 일관성을 해치고 입문자에게 개발을 어렵게 만드는 원인 중 하나입니다.

이러한 크로스브라우징의 문제를 해결하기 위해서 "Babel(바벨)"이 등장하였습니다. 바벨은 ECMAScript2015 이상으로 작성된 코드들을 "모든 브라우저에서 일관되게 동작"하도록 호환성을 지켜줍니다. 뿐만 아니라 타입스크립트, JSX 문법처럼 다른 언어로 분류되는 것도 브라우저에서 동작하도록 만들어 줍니다.

바벨 설치

바벨을 설치하기 위해서는 npm을 사용하여 설치합니다. 바벨에는 babel/core와 터미널 도구로 사용하기 위한 babel/cli를 설치해야 합니다. 터미널에 아래 명령어를 입력하여 설치합니다.

npm install @babel/core @babel/cli

명령어 입력 이후 프로젝트의 package.json파일의 dependencies에 설치된 것을 확인할 수 있습니다.

바벨의 기본 동작

바벨의 동작을 알아보기 위해서 바벨이 변환하는 코드를 작성해보겠습니다.

// app.js

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

const 키워드와 화살표 함수는 ES6의 문법입니다. 이는 ES6를 지원하지 않는 브라우저는 해당 코드를 해석할 수 없습니다. 이를 해결하기 위해서 바벨이 모든 브라우저에서 인식이 가능하도록 코드를 변환합니다.


설치한 바벨은 node_modules 폴더의 .bin 폴더 안에 존재하고 있습니다. 바벨을 실행하기 위해서는 node_modules/.bin/babel app.js 또는 npx babel app.js를 터미널에 입력합니다.

바벨을 실행하면 터미널에 코드가 출력된 것을 확인할 수 있습니다. 하지만 코드가 변환되지는 않았습니다.


바벨은 세 단계로 빌드를 진행합니다. 참고로 빌드란 바벨이 코드를 변환하는 작업을 의미합니다.

  1. 파싱(Parsing) : 코드를 받아 각 토큰들로 분해한 후 문법 구조에 맞게 트리 구조인 추상 구문 트리(AST)로 변환하는 단계입니다.

  2. 변환(Transforming) : 변환된 추상 구문 트리를 모든 브라우저에서 동작하도록 변환시켜주는 단계입니다. 이때 실제로 코드를 변경하는 작업을 합니다.

  3. 출력(Printing) : 변환된 결과물을 출력하는 단계입니다.

위에서 바벨을 실행했을 때 코드가 변환 단계를 거치지 않고 파싱 단계 이후 바로 출력 단계로 넘어간 것을 확인할 수 있습니다.

이처럼 바벨은 파싱과 출력만 담당하고 변환 단계는 처리하지 않습니다.변환 단계를 처리하기 위해서는 "플러그인"을 사용해야 합니다.

플러그인

웹팩에서 로더나 플러그인을 커스텀으로 만들 수 있듯이 바벨의 플러그인도 커스텀으로 만들 수 있습니다. 커스텀 플러그인을 만들어 플러그인의 동작을 살펴보겠습니다.

플러그인의 동작

바벨 플러그인을 위한 파일로 my-babel-plugin.js를 만들고, 아래 코드를 입력합니다.

// my-babel-plugin.js

module.exports = function myBabelPlugin() {
    return {
        visitor: {
            Identifier(path) {
                const name = path.node.name;
                
                // 바벨이 만든 AST 노드를 출력
                console.log('Identifier() name: ', name);
            }
        },
    };
}

바벨 플러그인은 visitor라는 프로퍼티를 가진 객체를 반환하는 함수로 구성되어 있습니다. visitor 프로퍼티는 객체가 바인딩되어 있으며, 이 객체는 바벨이 파싱하여 만든 추상 구문 트리에 접근할 수 있는 메서드를 제공합니다.
그 중에서 Identifier라는 메서드의 동작을 알아보겠습니다. 여기서 Identifier 메서드는 path라는 매개변수를 갖는데 객체를 전달 받습니다. path객체로는 node.name으로 파싱된 결과물에 접근하게 됩니다.

현재 해당 플러그인은 파싱된 AST를 콘솔에 출력하는 동작만을 하는 플러그인입니다.

플러그인의 사용 방법을 알아보게 위해서 npx babel --help를 입력하면 아래와 같이 터미널에 사용 방법이 출력됩니다.

그 중에서 --plugins [list] 옵션을 보면 list에 플러그인을 전달하여 바벨로 플러그인을 실행할 수 있습니다.

위에서 만든 커스텀 플러그인을 사용해보겠습니다. 터미널에 아래와 같은 명령어를 작성합니다.
npx babel app.js --plugins ./my-babel-plugin.js

플러그인의 visitor 객체에 존재하는 Identifier 메서드가 실행되어 콘솔에 출력된 것을 확인할 수 있습니다. Identifier 메서드에게 전달된 path 인자로 각 토큰에 접근할 수 있있다는 것을 확인할 수 있습니다.


이번에는 코드 문자열을 역순으로 변환해주는 동작을 추가하여 바벨로 빌드를 해보겠습니다.

// my-babel-plugin.js

module.exports = function myBabelPlugin() {
    return {
        visitor: {
            Identifier(path) {
                const name = path.node.name;
                
                // 바벨이 만든 AST 노드 출력
                console.log('Identifier() name: ', name);
                
                // 변환 작업: 코드 문자열을 역순으로 변환
                path.node.name = name
                    .split("")
                    .reverse()
                    .join("")
            }
        },
    };
}

이후 터미널에 npx babel app.js --plugins './my-babel-plugin.js를 입력하면 다음과 같은 결과가 출력됩니다.

마지막 출력 결과를 보면 alert, msg, window, msg 모두 역순으로 변환된 것을 확인할 수 있습니다. 즉, 파싱된 토큰들이 역순으로 변환되었습니다.


우리는 my-babel-plugin 파일이 app.js 코드를 ES5로 변환하려고 합니다.

첫 번째로 const라는 키워드를 var 키워드로 변환하는 플러그인을 작성해보겠습니다.

module.exports = function myBabelPlugin() {
    return {
        visitor: {
            VariableDeclaration(path) {
                console.log('VariableDeclaration() kind: ', path.node.kind);
                
                // 변환 작업: const를 var로 변환
                if (path.node.kind === 'const') {
                    path.node.kind = 'var'
                }
            },
        },
    };
}

visitor 객체의 VariableDeclaration 메서드도 path라는 매개변수에 파싱된 토큰을 전달받습니다. 그리고 path의 node.kind에 접근하면 const에 접근할 수 있습니다.

if 문에서는 path.node.kind가 const라면 var로 변환해주는 동작을 합니다.

해당 플러그인을 실행하면 아래와 같은 결과가 출력됩니다.

마지막 줄을 보면 const가 var로 변환된 것을 확인할 수 있습니다.

visitor 객체의 Identifier의 경우 식별자에 접근하며, VariableDeclaration 메서드는 변수 선언 키워드에 접근하는 것 같습니다.

플러그인 사용

@babel/plugin-transform-blcok-scoping

이전에 const를 var로 변환해주는 것이 "block-scoping 플러그인"이 변환해줍니다. 즉, block-scoping 플러그인은 const, let 처럼 블록 스코프를 따르는 에약어를 함수 스코프를 사용하는 var 키워드로 변환해줍니다.

npm을 통해 block-scoping 플러그인을 설치하기 위해서 아래 명령어를 입력합니다.

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

그리고 설치한 block-scoping 플러그인을 실행하기 위해서 아래 명령어를 입력합니다,

npx babel app.js --plugins @babel/plugin-transform-block-scoping

결과를 확인하면 이전 my-babel-plugin과 동일하게 변환된 것을 확인할 수 있습니다.

@babel/plugin-transform-arrow-functions

이번에는 ES6 문법인 화살표 함수를 ES5 문법으로 변환해주는 플러그인이 필요합니다. 우리는 "arrow-functions 플러그인"을 사용하여 화살표 함수를 일반 함수로 변환할 수 있습니다.

npm으로 해당 플러그인을 설치하기 위해서 아래 명령어를 입력합니다.

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

설치한 플러그인을 실행하기 위해서 아래 명령어를 입력합니다.

npx babel app.js --plugins @babel/plugin-trnasform-arrow-functions

화살표 함수가 일반 함수로 변환된 결과를 확인할 수 있습니다.


이번에는 block-scoping 플러그인과 arrow-functions 플러그인을 같이 사용해보겠습니다.

npx babel app.js --plugins @babel/plugin-transform-block-scoping --plugins @babel/plugin-transform-arrow-functions

변환된 결과를 확인해보면 const 키워드는 var로 변환되고, 화살표 함수는 일반 함수로 변환된 것을 확인할 수 있습니다.

@babel/plugin-transform-strict-mode

ES5 부터 지원하는 strict-mode를 사용하는 것이 안정하기 때문에 "use strict" 구문을 추가해주는 strict-mode 플러그인을 사용합니다.

strict-mode 플러그인을 설치하기 위해 터미널에 아래와 같은 명령어를 작성합니다.

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

설치한 이후에 플러그인을 실행하면 아래와 같이 변환된 결과를 확인할 수 있습니다.

npx babel app.js --plugins @babel/plugin-trnasform-strict-mode

파일 상단에 "use strict" 구문이 추가된 것을 확인할 수 있습니다.

babel.config.js

플러그인이 많아질 수록 플러그인을 실행하기 위한 명령어가 길어지기 때문에 웹팩 설정 파일인 webpack.config.js 파일처럼 바벨도 babel.config.js 파일을 기본 설정 파일로 작성할 수 있습니다.

프로젝트 루트에 바벨 설정 파일을 위한 babel.config.js 파일을 생성합니다. 그리고 아래와 같은 코드를 작성합니다.

// babel.config.js

module.exports = {
    plugins: [
        "@babel/plugin-trnasform-block-scoping",
        "@babel/plugin-trnasform-arrow-functions",
        "@babel/plugin-trnasform-strict-mode"
    ]
};

객체의 plugins라는 배열에 요소로 설치한 플러그인들을 작성해줍니다.

그리고 바벨을 실행할 때는 아래 명령어를 터미널에 입력합니다.
npx babel <filename>

바벨을 기본적으로 babel.config.js 파일을 읽어 플러그인을 적용하고 변환한 다음에 결과를 출력합니다.

프리셋

ES6로 작성할 때 필요한 플러그인을 일일이 설정하는 것은 비효율적입니다. 그래서 목적에 맞게 여러 프러그인을 모아놓은 것이 있는데 이를 "프리셋(preset)"이라고 합니다.

프리셋 동작

프리셋의 동작을 알아보기 위해서 커스텀 프리셋으로 작성해보겠습니다.

프로젝트 상단에 프리셋을 위한 my-babel-preset.js 파일을 생성하고 아래 코드를 작성합니다.

// my-babel-preset.js

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

프리셋은 객체를 반환하는 함수이며, 이때 반환되는 객채는 plugins라는 배열을 갖고 있습니다. plugins 배열에는 사용할 플러그인을 요소로 추가합니다. 이는 세 개의 플러그인을 갖는 하나의 프리셋이 됩니다.

이 프리셋을 사용하기 위해서 babel.config.js 파일을 아래와 같이 수정해줍니다.

// babel.config.js

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

그리고 바벨을 실행하면 이전 결과와 동일하게 출력됩니다. 프리셋을 사용하여 프리셋이 있는 모든 플러그인을 실행되도록 했습니다.

프리셋 사용하기

우리는 일일이 바벨 플러그인을 설치하여 사용하는 것보다는 목적에 따라 제공해주는 프리셋을 사용합니다.
아래는 대표적인 프리셋입니다.

  • preset-env

  • preset-flow

  • preset-react

  • preset-typescript

preset-env는 ES6+를 변환할 때 사용합니다. babel 7 이전 버전에서는 연도별로 각 프리셋을 제공했지만 현재는 preset-env 하나로 합쳐졌습니다.

preset-flow, preset-flow, preset-react, preset-typescript는 flow, 리액트, 타입스크립트를 변환하기 위한 프리셋입니다.

@babel/preset-env

먼저 preset-env를 설치하기 위해서 터미널에 다음과 같은 명령어를 사용합니다.

npm install @babel/preset-env

그리고 바벨 설정 파일인 babel.config.js 파일에 설치한 프리셋을 등록합니다.

// babel.config.js

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

이후 npx babel app.js 명령어로 빌드를 하면 다음과 같이 ES5 문법으로 변환된 결과가 출력됩니다.

ES6+ 문법이 ES5로 변환되고 "strict mode" 구문이 추가된 것을 확인할 수 있습니다.

preset-env 프리셋 설정과 폴리필

타겟 브라우저

타겟 브라우저는 프리셋을 사용할 때 이 프리셋으로 변환되는 코드는 특정 브라우저를 지원하도록 설정합니다.

presets 배열의 요소로 배열을 추가합니다. 이때 배열의 첫 번째 요소는 사용할 플러그인, 두 번째 요소로 옵션 객체를 작성합니다.

targets 옵션에 브라우저 버전명만 지정하면 env 프리셋은 이에 맞는 플러그인을 찾아 최적의 코드로 변환하여 출력합니다.

타겟 브라우저를 설정하기 위해서 바벨 설정 파일인 babel.config.js 파일을 다음과 같이 수정해줍니다.

// babel.config.js

module.exports = {
    presets: [
        ['@babel/preset-env', {
            targets: {
                // 크롬 79까지 지원하는 코드를 만든다
                chrome: '79'
            }
        }]
    ]
}

아래 결과를 보면 옵션 객체를 추가하기 전 결과와 다른 것을 확인할 수 있습니다. "use strict" 구문이 추가된 것은 동일하지만, const 키워드와 화살표 함수는 변환되지 않았습니다. 이는 chrome 79까지는 const와 화살쵸 함수를 사용할 수 있기 때문에 변환되지 않았습니다.

만약 chrome 뿐만 아니라 IE에서도 동작하는 코드를 만들기 위해서 옵션 객체를 다음과 같이 수정합니다.

targets: {
    chrome: '79',
    ie: '11'
}

이제 env 프리셋은 크롬 79버전과 IE 11 버전까지 동작하는 코드로 변환합니다.

폴리필

먼저 ES6의 Promise 객체를 사용하는 코드를 작성하고

// app.js

new Promise();

app.js 파일을 바벨로 빌드하면 다음과 같은 결과가 나옵니다.

env 프리셋으로 변환을 시도했지만 Promise 객체를 생성하는 코드는 변하지 않았습니다. target에 ie 11을 설정하고 빌드한 것인데 IE는 여전히 프라미스를 해석하지 못하고 에러가 발생하게 됩니다.

이 에러는 IE 브라우저가 Promise라는 식별자를 찾는데 어디에도 존재하지 않기 때문에 에러가 발생하게 됩니다.

여기서 알 수 있는 점은 env 플러그인이 Promise를 ES5 버전으로 변환헤줄 것이라고 예상했지만 바벨은 ES6를 ES5로 변환할 수 있는 것만 변환합니다. 그렇지 못한 것들은 "폴리플"이라고 부르는 코드 조각을 추가해서 해결해야 합니다.

ES5 버전으로 변환할 수 없는 것들은 해결하기 위해서 core-js라는 라이브러리를 사용하여 구현합니다. core-js 뿐만 아니라 babel-js도 존재합니다.

core-js를 프로젝트에 설치하기 위해서 터미널에 아래 명령어를 입력합니다.
npm install core-js@2

preset-env의 옵션 객체에는 targets 옵션 뿐만 아니라 useBuiltIns 옵션으로 폴리필의 사용 여부 옵션도 설정할 수 있습니다. babel.config.js 파일을 아래처럼 수정합니다.

// babel.config.js

module.exports = {
    presets: [
        ['@babel/preset-env', {
            targets: {
                chrome: '79',
                ie: '11'
            },
            useBuiltIns: 'usage', // 폴리필 사용 방식 지정
            corejs: {
                version: 2 // 폴리필 버전 지정
            }
        }]
    ]
}

useBuiltIns 옵션은 어떤 방식으로 폴리필을 사용할 지 지정하는 옵션입니다. 값으로 useage, entry, false 세 가지 값을 사용할 수 있으며, 기본값은 false로 지정되어 폴리필이 동작하지 않습니다.
usage나 entry로 지정하면 폴리필 패키지 중 core.js를 모듈로 가져옵니다. 이때 가져온 core.js 모듈의 버전도 명시하는데 기본값은 2입니다.

그리고 빌드를 하면 아래와 같은 결과가 출력됩니다.

require 구문이 추가된 것을 확인할 수 있습니다. 이는 core-js 라이브러리로부터 Promise 폴리필 파일을 모듈로 가져오는 구문이 추가되어 IE 브라우저에서도 Promise를 사용할 수 있게 되었습니다.

웹팩으로 통합

실제로는 바벨로 변환하는 작업을 직접 터미널에 명령어를 입력하여 빌드하는 것보다는 웹팩으로 통합하여 사용하는 것이 일반적입니다. 즉, 웹팩으로 빌드할 때 그 과정에서 바벨로 변환하여 통합하여 사용하는 것이 일반적입니다.

바벨은 웹팩의 로더 형태로 제공되는데 이때 사용되는 패키지가 babel-loader 입니다. 이를 설치하기 위해서 아래 명령어를 터미널에 작성합니다.

npm install babel-loader

설치한 로더를 사용하기 위해서 웹팩 설정 파일인 webpack.config.js 파일의 module.rules 배열에 요소로 추가해줍니다.

// webpack.config.js

module.exports = {
    ,,,
    module: {
        rules: [
            {
                test: /\.js/,    // js 파일 선택
                loader: 'babel-loader',    // babel-loader 사용
                exclude: /node_modules/    // 처리 제외할 파일
            }
        ]
    }
};

test 프로퍼티에는 모든 js 파일이 해당 로더로 실행하도록 패턴을 작성하고, loader 프로퍼티에는 사용할 로더인 babel-loader를 작성합니다.
그리고 exclude 프로퍼티에는 babel-loader가 처리하지 않도록 제외시키는 옵션도 추가했습니다. 즉, node_modules에 존재하는 js 파일은 로더가 처리하지 않습니다.

이후 npm run build 명령어를 통해 빌드를 하면 에러가 발생합니다.

app.js 파일을 babel-loader가 변환시킵니다. 그 결과 코드 상단에는 core-js , 즉 폴리필을 import 하는 구문이 추가되어 있고 웹팩은 이 import 구문을 보고 core-js를 찾는데 node_modules 폴더 안에 존재하지 않기 때문에 위와 같은 에러가 발생하게 됩니다.

즉, 프로젝트에 core-js 패키지를 설치해야 합니다. 터미널에 core-js 패키지를 설치하기 위한 명령어를 작성합니다.

npm install core-js@2

@2는 2 버전을 설치하겠다고 특정한 것입니다. npm으로 패키지를 설치할 때 특정 버전으로 설치하고 싶다면 패키지명 다음에 @버전명을 뒤에 붙여줍니다.

이후에 다시 웹팩으로 빌드하면 다음과 같이 빌드가 성공적으로 이루어진 것을 확인할 수 있습니다.


정리

바벨은 일관적인 방식으로 코드를 작성하고, 다양한 브라우저에서 동일하게 동작하는 어플리케이션을 만들기 위한 도구입니다.

바벨의 코어는 파싱과 출력만 담당하며, 변환 단게는 플러그인이 처리합니다.

여러 개의 플러그인을 모아놓은 것이 프리셋이며, 가장 흔하게 사용되는 프리셋으로 preset-env 프리셋이 있습니다.

바벨이 변환하지 못하는 코드는 폴리필이라고 불리는 코드 조각을 가져와서 사용합니다.

babel-loader는 웹팩과 함께 사용하면 훨씬 단순하고 자동화된 프론트 엔드 개발환경을 갖출 수 있습니다. 먼저 바벨로 코드를 변환한 뒤에 변환된 파일을 하나의 번들로 만듭니다.

profile
Frontend Dev

0개의 댓글