toggle menu

babel-polyfill 과 babel-plugin-transform-runtime 그리고 IE8

2017. 8. 5. 15:14 JavaScript

Babel

BabelES2015+ 문법을 ES5 지원 브라우저에서 해석할 수 있도록 변환해주는 트랜스파일러이다. 하지만 새롭게 추가된 전역 객체들(Promise, Map, Set..)과 String.padStart 등 전역 객체에 추가된 메서드들은 트랜스파일링만으론 해결하기 어렵기 때문에 core-jsregenerator-runtime 와 같은 별도의 polyfill 이 필요하다.

Babel 기반에서 폴리필을 추가하는 방법은 두 가지가 존재한다. babel-polyfill 을 사용하는 방법과 babel-plugin-transform-runtime 을 사용하는 방법이다. 차례대로 살펴보자.

 

 

 

babel-polyfill

babel-polyfill 은 내부적으로 페북에서 만든 Generator Function 폴리필인 regenerator runtime 과 ES5/6/7 폴리필인 core-js 를 주요 디펜던시로 가지고 있다. 가장 유명하고 안정적인 폴리필들을 사용하기 편리하게 랩핑해 놓은 모듈이라고 생각하면 편하다.

babel-polyfill을 사용하려면 먼저 아래와 같이 npm 으로 설치해준다.

# babel-polyfill 디펜던시 추가
$ npm install --save babel-polyfill


그런 뒤에 스크립트 중 최상위 시작지점에 require 혹은 import 해주면, babel-polyfill 은 전역에 직접 폴리필을 추가한다. 즉, 전역 오염이 발생한다.

require('babel-polyfill');

//혹은 

import 'babel-polyfill';

babel-polyfill 을 사용할 때의 장점은 어떤게 있을까?

core-js는 먼저 전역에 폴리필을 추가하기전에 해당 기능이 있는지를 체크하므로, 폴리필이 필요없는 최신 브라우저에서는 폴리필없이 동작하게 되어 (babel-plugin-transform-runtime 를 사용하는 것에 비해) 빠르다.

또 전역 객체를 직접 수정하기 때문에 Array.prototype.includes 등 ES2015+에서 새로 추가된 프로토타입 메서드도 문제없이 사용 가능하고, 덕분에 내가 짠 코드가 아닌 npm에서 디펜던시로 받은 라이브러리가 ES2015+ 에서 새롭게 추가된 객체나 프로토타입 메서드를 사용하는지 신경쓸 필요가 없게 되어 개발할 때도 편리하다.

babel-polyfill 을 사용할 때의 단점이라면 실제 사용하지 않는 폴리필도 몽땅 추가되므로 번들링되는 파일의 크기가 커지게 된다는 점이다.




babel-polyfill 사용 시 주의할 점

한가지 꼭 주의할 부분이 있는데, 한 페이지에서 딱 하나의 babel-polyfill 만 허용된다는 점이다. 두 개의 babel-polyfill 을 실행하게 되면 반복적인 전역 객체 수정을 막기 위해 아래의 오류를 발생시킨다.

Uncaught Error : only one instance of babel-polyfill is allowed


babel-polyfill 소스를 살펴보면 내부적으로 전역 변수 하나를 두어 babel-polyfill 이 이미 실행되었는지를 체크하고 이미 실행되었다면 무조건(!!!) 오류를 던지는 구조이다.

if (global._babelPolyfill) {
  throw new Error("only one instance of babel-polyfill is allowed");
}
global._babelPolyfill = true;

import "core-js/shim";
import "regenerator-runtime/runtime";

...


왜 이렇게 극단적으로 오류를 던지게 했을까?

babel-polyfill 의 디펜던시인 core-js 의 ES6/7 폴리필의 경우 두 번 호출되면 내부적으로 오류가 발생되어 정상적으로 폴리필이 적용되지 않는다. 아마도 core-js 가 가지는 이러한 문제점 때문에 babel-polyfill 에 이런 예외처리가 추가된 게 아닐까 추정된다. 

따라서 babel-polyfill 이 두 번 호출되지 않도록 주의해야 한다. 단순히 babel-polyfill 이 두 번 호출되는 것 뿐만 아니라 core-js 의 ES6/7 폴리필이 로드된 상태에서 babel-polyfill 이 로드되거나 babel-polyfill 이 로드된 상태에서 core-js 의 ES6/7 폴리필이 로드되는 것 역시 주의해야 한다.

정리해보면, 싱글 페이지 어플리케이션에서 또다른 폴리필이 호출될 염려가 전혀 없는 환경이라면, babel-polyfill 만으로 IE8 과 같은 레거시 브라우저에서 큰 문제없이 트랜스파일링된 코드를 실행시킬 수 있기 때문에 babel-polyfill 은 가장 편하고 빠르게 폴리필을 적용할 수 있는 방법으로 추천 할 만하다.

 

 

 

babel-plugin-transform-runtime

babel-polyfill 외에 또 하나의 방법은 babel-plugin-transform-runtime 플러그인을 사용해서 Babel 이 트랜스파일링을 하는 과정에서 폴리필이 필요한 부분을 내부의 헬퍼 함수를 사용하도록 치환해주는 방법이다.

Babel 은 내부적으로 _extend 처럼 공통 함수를 위한 작은 헬퍼들을 사용하는데, 기본적으로 모든 파일에 이런 헬퍼들이 추가되기 때문에 이런 낭비를 막기 위해 transform-runtime 플러그인을 사용해서 모든 헬퍼들을 babel-runtime 이라는 모듈을 참조하도록 해준다. babel-plugin-transform-runtime 은 플러그인 이기때문에 빌드 단계(트랜스파일링 단계)에서 동작하게 된다.

transform-runtime 은 코드에 대한 샌드박스도 제공해주는데 내부적으로 core-js 내장하고 여기에 대한 alias 를 생성해서 트랜스파일링 과정에서 폴리필이 필요한 부분들이 이 alias 를 참조하도록 변경해서 전역 오염없이 폴리필이 적용되도록 만들게 된다.

babel-plugin-transform-runtime 플러그인은 내부적으로 babel-runtime 을 디펜던시로 갖는데, babel-runtime 은 또 core-js 와 regenerator-runtime 을 디펜던시로 갖는다. 자동적으로 babel-runtime/regenerator 와 babel-runtime/core-js 를 내장하게 되며 트랜스파일링 과정에서 인라인 Babel 헬퍼들을 제거하고 babel-runtime/helper 들을 사용하도록 변경하게 된다.


babel-plugin-transform-runtime 플러그인을 사용하려면 디펜던시는 아래의 두 가지 모두가 필요하다.

# babel-plugin-transform-runtime 디펜던시 추가
$ npm install --save-dev babel-plugin-transform-runtime

# babel-runtime 디펜던시 추가
$ npm install --save babel-runtime

babel-plugin-transform-runtime 은 트랜스파일링 과정에서 헬퍼들을 babel-runtime 모듈이 참조하도록 변경해주는 역할을 하고, babel-runtime 은 실제 실행 환경에서 헬퍼함수들이 참조할 수 있는 폴리필을 내장한 모듈로서 동작한다.

babel-plugin-transform-runtime 을 설정하는건 웹팩 기준으로는 webpack config 에서 babel 의 plugins 에 추가해주기만 하면 된다. 웹팩 설정 파일은 1.x 버전 기준이다. 웹팩 2.x 버전부터는 IE8 지원을 하지 않기 때문에 IE8 지원을 위해 폴리필이 필요한 상황이라면 웹팩 1.x 버전을 사용하는 것이 좀더 안전할 것으로 보인다. (웹팩 2.x 이상 버전도 IE8 에서 동작하기는 하는 것 같다)

loaders: [
    //Babel 변환
    {
        test: /\.(js|jsx)$/,
        include: [
            /src\/js/,
            /node_modules\/axios/
        ],
        loader: 'babel',
        query: {
            cacheDirectory: true,

            //plugins는 presets 보다 먼저 실행되며,
            //plugins내의 순서는 처음 -> 나중으로 실행된다.
            plugins: [
                // ['transform-class-properties'],
                // ['transform-object-rest-spread', { useBuiltIns: true }],
                ['transform-runtime'],
            ],
            
            //presets내의 순서는 나중 -> 처음으로 실행된다.
            presets: [
                ['env', {
                    targets: {
                        browsers: ['ie >= 8']
                    },
                    loose: true,
                }],
                // ['react'],
            ],
        }
    },

    ...
],

앞서 설명한 것처럼 babel-plugin-transform-runtime 플러그인은 plugins 항목에 transform-runtime 을 추가해주는 것만으로 충분하다.


그 외에 주목해서 볼 부분은, include 로 node_modules 아래에 위치한 axios 를 추가해준 부분이다. 보통 트랜스파일링 대상에서 node_modules 아래에 있는 모듈들은 제외하는 것이 상식이다. 그런데 왜 추가해줬을까? 

axios 는 내부적으로 Promise 를 사용하는 라이브러리인데 Promise 가 전역 레벨에 선언되어 있지 않으면 오류가 발생하게 된다. 앞서 설명한 것처럼 babel-plugin-transform-runtime 플러그인은 전역 레벨에 폴리필을 적용하는 방식이 아니기 때문에 axios 가 실행될 때 Promise 를 찾지 못해 오류가 발생하게 되는 것이다. 이런 오류를 막기 위해 axios 도 babel-plugin-transform-runtime 플러그인을 통해 트랜스파일링 될 수 있도록 include를 사용해서 트랜스파일링 대상으로 추가해주어야 하는 것이다. 이런 부분이 babel-plugin-transform-runtime 플러그인을 사용할 때 주의할 부분이다.


babel-plugin-transform-runtime 플러그인은 아래와 같이 추가적인 옵션을 지정해줄 수 있으나 대부분 기본값으로 충분하다.

["transform-runtime", {
    "helpers": true, // 인라인 바벨 헬퍼 (classCallCheck, extends, 등)을 moduleName. 으로 치환
    "polyfill": true, // Promise, Set, Map 과 같은 새로운 내장 객체를 전역 오염없이 내부 객체를 참조하도록 변환
    "regenerator": true, // generator 함수를 전역 오염없이 regenerator runtime 을 사용하도록 변환
    "moduleName": "babel-runtime" // 헬퍼를 가져올 때 사용할 모듈의 이름
}]




babel-plugin-transform-runtime 플러그인이 적용된 코드

아래와 같은 간단한 코드를 babel-plugin-transform-runtime 을 사용하면 어떻게 트랜스파일링 될까?

class Person {
}


앞서 설명한 것처럼 babel-plugin-transform-runtime 은 트랜스파일링 과정에서 헬퍼들을 babel-runtime 모듈이 참조하도록 변경해주는 역할을 하고, babel-runtime 은 실제 실행 환경에서 헬퍼함수들이 참조할 수 있는 폴리필을 내장한 모듈로서 동작하게 된다.

"use strict";

var _classCallCheck2 = require("babel-runtime/helpers/classCallCheck");

var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

var Person = function Person() {
  (0, _classCallCheck3.default)(this, Person);
};


babel-plugin-transform-runtime 을 사용하게 되면 트랜스파일링이 필요한 부분이 너무 많을 경우 용량이 증가할 수 있으나 대체적으로 babel-polyfill 을 사용할 떄보다는 용량이 줄어든다.

하지만 babel-polyfill 과 달리 최신 브라우저에 관계없이 폴리필 코드가 무조건 물려서 동작하기 때문에 더 느려질 수 있다.




babel-plugin-transform-runtime 사용 시 주의할 점

그리고 치명적인 단점이 있는데, Object.assign 처럼 내장 객체의 static 메서드는 사용가능하지만, 새롭게 추가된 인스턴스 메서드 예를들면, [1,2,3,4,5].includes(3) 과 같은 메서드는 트랜스파일링 되지 않기 때문에 오류가 발생한다. 따라서 새롭게 추가된 프로토타입 메서드들은 사용하지 않도록 주의해야 한다.

또 한가지 주의할 점은 디펜던시들의 ES2015 기능들 사용 여부이다. babel-polyfill 은 전역을 직접 수정하기 때문에 npm 을 통해 설치한 디펜던시들이 Promise 와 같은 새로운 내장 객체를 사용하는지 여부를 걱정할 필요가 없지만, babel-plugin-transform-runtime 을 사용하면 이 부분까지 고려해주어야 한다. 예를들어 내부적으로 Promise 를 사용하는 디펀덴시가 있다면 이 디펜던시도 함께 트랜스 파일링 될 수 있도록 바벨 설정을 추가해주어야 한다. 앞의 예시에서는 axios 가 Promise 를 사용하기 때문에 include 로 트랜스파일링 대상에 추가해주었었다.

이런 불편함이 있지만, 전역을 오염시키지 않기 때문에 라이브러리 형태로 동작하는 경우 babel-polyfill 보다는 babel-plugin-transform-runtime 을 사용하는게 바람직하다. 내 라이브러리가 사용될 곳에서 이미 babel-polyfill 을 쓰고 있을 수도 있기 때문이다.

 


 

babel-plugin-transform-runtime 과 IE8

babel-plugin-transform-runtime 을 사용해서 IE8을 지원하려면, babel 은 기본적으로 ES5 환경이라고 가정하므로 core-js 의 es5 polyfill 은 필수적으로 필요하다. 앞서 core-js 가 여러번 호출될 경우 오류가 발생한다고 했었는데, 다행히 core-js 의 ES5 폴리필은 여러번 호출되어도 오류가 발생하지 않는다.

import 'core-js/es5';

babel 설정은 앞서 살펴본 것처럼 ES2015+ 관련 프리셋의 loose 모드를 활성화하고, transform-runtime 플러그인을 추가해주면 된다. 여기에서는 바벨의 env 프리셋으로 ES2015 관련 프리셋들을 추가해주고 있다. loose 모드를 활성화하지 않으면 IE8 과 호환성이 없는 코드로 트랜스파일링 되므로 주의해야 한다.

loaders: [
    //Babel 변환
    {
        test: /\.(js|jsx)$/,
        include: [
            /src\/js/,
            /node_modules\/axios/
        ],
        loader: 'babel',
        query: {
            cacheDirectory: true,
            plugins: [
                ['transform-runtime'],
            ],            
            presets: [
                ['env', {
                    targets: {
                        browsers: ['ie >= 8']
                    },
                    loose: true,
                }],
            ],
        }
    },

    ...
],

그런데 이렇게 Babel을 사용해서 transform을 해줘도 실제로 번들링된 결과물에는 여전히 IE8 에서 오류가 발생하는 default, catch 등의 키워드에 대한 처리가 이루어지지 않는다.

그래서 한번더 최종적으로 es3ify-loader 혹은 babel-plugin-transform-es3-member-expression-literals, babel-plugin-transform-es3-property-literals 플러그인으로 트랜스파일링을 해주어야 완전하게 IE8과의 호환성을 갖추게 된다.

postLoaders: [
    {
        test: /\.js$/,
        loader: 'es3ify-loader',
    }
]

또 uglify 를 해줄 경우 screw_ie8 옵션을 true 로 줄경우 IE8에서 오류가 발생하므로 반드시 이 옵션을 false 로 주어야 한다.

new webpack.optimize.UglifyJsPlugin({
    compressor: {
        screw_ie8: false,
        warnings: false
    },
    mangle: {
        screw_ie8: false
    },
    output: {
        comments: false,
        screw_ie8: false
    }
}),

 

 

 

마치며

IE8 을 지원해야 하는 안타까운 상황이 꽤 오래 가고 있다. 점점 라이브러리들이 IE8 지원을 중단해가는 상황인데 국내에서도 IE8 을 지원하지 않는 상황이 빨리 오기를 기대한다. (최근 개편한 네이버 메인 페이지가 IE7을 지원하는 상황이니 언제쯤에야 그런 날이 올지 모르겠다) 



 

참조

https://babeljs.io/docs/usage/caveats/

https://babeljs.io/docs/usage/polyfill/

http://babeljs.io/docs/plugins/transform-runtime/

JavaScript 관련 포스팅 더보기