FE개발자의 성장 스토리 02 : Babel7과 corejs3 설정으로 전역 오염 없는 폴리필 사용하기

안녕하세요, FE플랫폼팀 jeff 입니다.
저는 카카오톡 웹톡 서비스를 개발하고 있습니다. 웹톡 서비스고객 사이트에 적용할 수 있는 카카오톡 상담톡/챗봇 대화 솔루션 입니다. 최근 해당 서비스를 개발하면서 고민하고 개선한 내용들을 팀 내에서 발표했는데요, 저와 비슷한 고민이 있으신 다른 분들에게 조금이나마 도움이 되고자 발표 내용을 공유하기 위해 글을 작성하게 되었습니다. : )

 

kakao 고객센터 페이지에 삽입된 웹채팅 서비스

 

전역 오염 이슈

 

웹채팅과 같이 고객 사이트에 삽입되는 서비스는 주의할 점이 있습니다. 바로 고객 사이트의 전역 스코프를 오염시키지 않는 것입니다. 전역 스코프가 오염될 경우, 고객 페이지에서 이름 충돌이 발생하고, 예측하기 힘든 에러가 발생할 수 있습니다. 기존 웹채팅에서는 바벨 폴리필(babel polyfill)이 전역 스코프에 삽입되어 전역 오염 이슈가 있었습니다.

global scope가 오염된 Internet Explorer 11 환경의 테스트 페이지

 

Babel Polyfill

 

먼저 전역 스코프를 오염시킨 babel polyfill을 살펴보겠습니다.

  • 바벨(Babel)은 source to source compiler입니다. 바벨은 ES6 코드를 ES5 코드로 변환하는 구문 변환(syntax transform)을 수행합니다.
  • 폴리필(Polyfill)은 구형 브라우저에서 지원하지 않는 기능을 제공하는 코드를 의미합니다. ES6Promise 같은 객체들은 ES5에 존재하지 않으므로 구문 변환 뿐만 아니라 해당 객체들을 정의하는 코드인 바벨 폴리필(babel polyfill)을 추가해야 합니다.

 

폴리필(polyfill)은 충전솜이라는 의미를 가지고 있습니다.
ES5에 비어있는 ES6 객체, 메소드들을 충전솜처럼 폴리필이 채워줍니다

 

웹채팅 서비스는 Internet Explorer 11(IE11) 브라우저가 지원 대상이기 때문에, IE11을 위한 바벨 폴리필을 추가해야 합니다. 과거에는 @babel/polyfill 패키지를 직접 전역 스코프에 가져오는(import) 방식으로 바벨 폴리필을 추가했지만 deprecated 되었습니다. 현재는 core-js/stableregenerator-runtime/runtime 패키지를 직접 전역 스코프에 삽입합니다. 이러한 바벨 폴리필 삽입 방법은 웹채팅처럼 고객 페이지에 삽입되는 애플리케이션인 경우 고객의 전역 스코프를 오염시키는 문제가 있습니다.

전역 오염 문제 해결을 위해 바벨 설정 파일(Babel Config File)을 확인합시다!

 

Babel Config File

 

기존 바벨 설정 파일(Config File)인 .babelrc.json을 살펴보겠습니다. .babelrc.json으로 파일 이름을 설정하면 바벨이 자동으로 해당 파일 설정으로 source to source compile을 진행합니다.
설정 파일에서 정의하는 객체는 presetsplugins 프로퍼티를 가집니다. 여기서 preset은 plugin들의 집합을 의미하므로, 바벨 설정은 다르게 말하면 바벨 플러그인 설정입니다.
바벨 플러그인은 바벨 컴파일 단계에서 AST(Abstract Syntax Tree)를 변형하는 역할을 수행합니다. 바벨은 플러그인이 변형시킨 AST를 가지고 타깃 코드를 생성합니다.

바벨 컴파일 과정

 

기존 바벨 설정 파일은 다음과 같습니다. preset 필드는 @babel/preset-env, @babel/preset-react로 구성되어 있고, plugin 필드에는 styled-jsx/babel이 있습니다. styled-jsx/babel의 경우 styled-jsx 문법 변환을 위해 추가한 것이므로, preset-envpreset-react를 살펴보겠습니다.

//.babelrc.json
{
  "presets": [
    ["@babel/preset-env", {"useBuiltIns":"entry", "corejs":3}],
    ["@babel/preset-react"]
  ],
  "plugins": [
    ["styled-jsx/babel", {"optimizeForSpeed": true, "vendorPrefixes": true, "sourceMaps": false}]
  ]
}

개선이 필요한 기존 바벨 설정 파일

 

@babel/preset-react

@babel/preset-react 프리셋JSX 코드를 React.CreateElement호출 코드로 변환합니다. 현재 특별한 설정 추가가 필요 없는 preset입니다.

//바벨 컴파일 전
const profile = (
  <div>
    <img src="profile.png" className="profile" />
    <h1>{[user.firstName, user.lastName].join(" ")}</h1>
  </div>
);

//바벨 컴파일 후
const profile = React.createElement(
  "div",
  null,
  React.createElement("img", { src: "profile.png", className: "profile" }),
  React.createElement("h1", null, [user.firstName, user.lastName].join(" "))
);

preset-react 프리셋 설정으로 진행한 바벨 컴파일 결과

 

@babel/preset-env

@babel/preset-env 프리셋은 타깃 환경에 필요한 구문 변환(syntax transform), 브라우저 폴리필(browser polyfill)을 제공합니다. @babel/preset-env의 하위 옵션에 대해 살펴보겠습니다.

 

target 옵션

@babel/preset-env 프리셋이 타깃 브라우저에 필요한 플러그인만 삽입하도록 설정하는 옵션입니다. 해당 옵션이 없으면 모든 ES2015+를 변환하므로 필요한 변환만 하기 위해선 필요합니다.

//browserslist-compatible query
{
  "presets": [
    ["@babel/preset-env", {
      "targets": "> 0.25%, not dead"
    }],
  ]
}

//mininum version
{
  "presets": [
    ["@babel/preset-env", {
      "targets": {"chrome": "58", "ie": "11"}
    }],
  ]
}

target 옵션 설정 예시, 브라우저 점유율 또는 명시적인 버전으로 설정합니다.

 

useBuiltIns 옵션

preset-env 프리셋의 폴리필 삽입 방식을 설정하는 옵션입니다. 옵션 값으로 usage, entry, false를 사용할 수 있습니다. false 이외의 옵션을 사용하면 최신 자바스크립트 폴리필이 포함된 standard javascript library인 core-js 모듈을 가져오는(import) 코드를 타깃 브라우저에 맞게 삽입/수정합니다. 옵션 값에 따른 폴리필 삽입 방식을 살펴보겠습니다.

useBuiltIns:entry 설정은 core-js/stableregenerator-runtime/runtime 모듈을 전역 스코프에 직접 삽입한 경우에만 가능합니다. 해당 설정은 전체 core-js 삽입문(import)을 corejs 하위 특정 모듈들의 삽입문(import)으로 변경시켜, 타깃 환경에 필요한 폴리필만 전역 스코프에 추가되도록 합니다.

//in
import "core-js";

//out
import "core-js/modules/es.string.pad-start";
import "core-js/modules/es.string.pad-end";

useBuiltIns:entry설정 시 core-js 패키지 삽입문(import)를 타겟 브라우저에 필요한 개별 패키지 삽입문으로 수정합니다.

 

useBuiltIns:usage 설정은 실제 사용한 폴리필만 삽입합니다. import 문 변경이 아닌 삽입이므로 core-js/stableregenerator-runtime/runtime 모듈을 전역 스코프에 삽입하지 않아도 됩니다.

//in
var a = new Promise();
var b = new Map();

//out(실제 사용한 폴리필만 삽입하도록 import 문이 추가된다)
import "core-js/modules/es.promise";
import "core-js/modules/es.map";
var a = new Promise();
var b = new Map();

useBuiltIns:usage 설정 시 실제 사용한 폴리필만 삽입합니다

 

corejs 옵션

corejs 옵션은 useBuiltIns 옵션과 함께 사용해야 합니다. 해당 옵션은 삽입되는 core-js 모듈의 버전을 설정합니다. default 값은 2이고, version 2는 업데이트가 중단되었기 때문에 현재는 version 3를 사용해야 합니다.

corejs@2에서 corejs@3로 버전이 올라가면서 몇 가지 변화가 생겼습니다.

 

기존 바벨 설정 파일 정리

  • presets
  • @babel/preset-env : 타겟 브라우저에 필요한 구문 변경(syntax transform)과 브라우저 폴리필(browser polyfill)을 제공하는 프리셋
    • useBuiltIns:entry : 전체 core-js 삽입문(import)을 corejs 하위 특정 모듈들의 삽입문(import)으로 변경
    • corejs: 3 : corejs version 3를 삽입
  • @babel/preset-react : JSX 코드를 React.CreateElement호출 코드로 변환
  • plugins
  • styled-jsx/babel : styled-jsx 문법을 변환해주는 플러그인
{
  "presets": [
    ["@babel/preset-env", {"useBuiltIns":"entry", "corejs":3}],
    ["@babel/preset-react"]
  ],
  "plugins": [
    ["styled-jsx/babel", {"optimizeForSpeed": true, "vendorPrefixes": true, "sourceMaps": false}]
  ]
}

기존 babel 설정 파일 리마인드

 

전역 스코프를 오염시키지 않도록 바벨 설정 파일을 개선합시다!

 

@babel/plugin-transform-runtime & core-js@3

@babel/transform-runtime 플러그인과 core-js@3를 사용하면 core-js-pure 패키지에서 폴리필을 삽입합니다. core-js-pure는 전역 스코프를 오염시키지 않는 core-js version입니다.
babel 7.4.0 이후 버전부터 transform-runtime 플러그인이 core-js@3 지원을 시작했습니다. 이전 버전과 달리 version 3는 인스턴스 메서드를 지원합니다.

//in
var sym = Symbol();
var promise = Promise.resolve();
var check = arr.includes("yeah!");
console.log(arr[Symbol.iterator]());

//out
import _getIterator from "@babel/runtime-corejs3/core-js/get-iterator";
import _includesInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/includes";
import _Promise from "@babel/runtime-corejs3/core-js-stable/promise";
import _Symbol from "@babel/runtime-corejs3/core-js-stable/symbol";

var sym = _Symbol();
var promise = _Promise.resolve();
var check = _includesInstanceProperty(arr).call(arr, "yeah!");
console.log(_getIterator(arr));

transform-runtime플러그인에 corejs@3옵션이 추가된 설정으로 바벨 트랜스파일링 수행

 

사용법

core-js@3@babel/runtime를 함께 사용하려면 @babel/transform-runtime 플러그인에 corejs:3옵션을 추가하고 @babel/runtime-corejs3 패키지를 설치합니다.


transform-runtime 플러그인을 도입하여 기존 바벨 설정 파일 개선

preset-env 프리셋의 useBuiltIns 옵션을 제거하고 target 옵션을 추가합니다. @babel/plugin-transform-runtime 플러그인을 추가하고 옵션으로 corejs:3를 설정합니다. 위와 같이 설정 변경 시, useBuiltIns 옵션에 의해 직접 삽입되었던 core-js 폴리필 대신 core-js-pure 폴리필이 삽입됩니다.

//.babelrc.json
{
    "presets": [["@babel/preset-env",{"target": {"ie": 11}}], ["@babel/preset-react"]],
    "plugins": [["@babel/plugin-transform-runtime",{"corejs":3}]]
}

이제 바벨 파일 설정이 끝났습니다. 하지만 아직 IE11 환경에서 에러가 발생합니다. 폴리필이 전역 스코프에서 사라지면서 ES6를 사용한 npm 모듈에서 에러 로그를 띄우기 시작합니다. 기존 웹 팩(webpack) 설정 파일에서 성능을 위해 node_modules 하위 npm 모듈들을 babel-loader 대상에서 제외했기 때문입니다. ES6를 사용한 npm 모듈들도 core-js-pure 폴리필을 사용할 수 있도록 babel-loader 대상에 포함시켜야 합니다.

 

ES6 syntax를 사용하는 npm module들

 

babel-loader

 

웹 팩 설정 파일의 babel-loader 설정을 보면 exclude 필드에서 node_modules 디렉토리를 모두 포함하는 정규 표현식을 사용하고 있습니다. negative-lookahead 정규 표현식 문법을 사용해서 ES6를 사용하는 npm 모듈들은 exclude 필드에서 제외하도록 수정합니다.

//webpack.config.js
module.exports = {
  ...,
  module: {
    rules: [{
        test: /\.(js|jsx)$/,
        exclude: /node_modules\/(?!(axios|@redux-saga|redux-logger))/,
        use: {
          loader: 'babel-loader'          
        },
      }]}}

수정한 babel-loader 객체

 

하지만 여전히 에러가 발생합니다. 현재 바벨 설정 파일 포맷은 .babelrc.json으로, 해당 파일이 속한 package에만 바벨 설정이 적용되는 file-relative configuration입니다. node_modules 하위 모듈들은 각각 package.json 파일이 있으므로 현재 바벨 설정이 모듈들에 적용되지 않습니다. 이때 babel7에 새로 등장한 project-wide configuration 설정 파일 포맷이 필요합니다.

 

Project Wide Configuration

 

바벨은 2가지 설정 파일 포맷이 있습니다. Project-wide 설정 파일과 File-relative 설정 파일입니다. 현재 필요한 Project-wide 설정을 하는 2가지 방법이 있습니다.

  • babel.config.json설정 파일을 루트 디렉터리에 위치
  • 웹 팩의 babel-loader 설정 중 configFile 옵션에서 바벨 설정 파일을 명시적으로 설정

첫 번째는 프로젝트의 루트 디렉터리 위치에 babel.config.json 포맷의 바벨 설정 파일을 놓는 방법입니다. 이 경우 바벨이 babel.config.json 파일을 자동으로 찾아서 project-wide로 바벨 설정을 진행합니다. 두 번째 방법은 웹 팩의 babel-loader 설정 중 configFile 옵션에서 바벨 설정 파일을 명시적으로 설정하는 방법입니다. 이 경우 바벨은 babel.config.json 파일을 자동으로 찾지 않고, 설정한 configFile을 적용합니다. 두 번째 방법을 사용하면 웹 팩의 번들별로 바벨 설정을 다르게 줄 수 있는 장점이 있어 두 번째 방법을 선택했습니다.

하지만 또 다른 에러가 발생합니다. 바벨을 적용한 npm 모듈들을 제대로 가져오지 못하는 문제가 생겼습니다. CommonJS 모듈의 내보내기 방식을 사용한 npm 모듈을 ES Module(ESM)의 가져오기 방식으로 가져오면서 문제가 발생했습니다.

모듈을 제대로 가져오지 못해 에러가 발생합니다.

 

ES Module과 CommonJS Module 같이 사용하기

두 모듈의 가져오기, 내보내기 방법이 달라 함께 사용하려면 추가적인 처리가 필요합니다.

바벨은 _interopRequireDefault라는 함수를 사용해 ES Module(ESM) 코드를 CommonJS 모듈의 문법으로 변환하여 두 모듈을 같이 사용할 수 있도록 합니다. 여기서 interop은 interoperability 단어를 의미하는데, 상호운용성이라는 뜻을 가지고 있습니다. 바벨은 ESM의 내보내기 문법을 사용한 모듈에는 module.exports.__esModule 플래그를 true로 설정합니다. 그리고 ESM 형식의 기본값 가져오기 코드를 만나면 _interopRequireDefault() 함수를 생성하고, 가져온 객체를 그대로 사용하지 않고 객체의 default 프로퍼티에 접근하여 사용하도록 코드를 변경합니다. 이렇게 바꿔주면 CommonJS 모듈은 module.exports에 할당한 객체가 새로운 객체의 default 프로퍼티에 할당되어 감싸진 상태로 반환됩니다. 이런 방식으로 CommonJS 모듈과 ES 모듈을 함께 사용할 수 있습니다.

"use strict";

var _foo = _interopRequireDefault(require("foo"));

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

interopRequireDefault 함수

 

위 설정을 위해선 @babel/plugin-transform-modules-commonjs 플러그인을 추가해야 합니다. preset-env 프리셋의 modules 옵션을 commonjs 또는 cjs로 설정하면 transform-module-commonjs 플러그인이 추가됩니다.

 

최종 설정

 

최종 설정은 다음과 같습니다.

//.babelrc.json, configFile로 지정하여 Project-wide config 적용
{
    "presets": [["@babel/preset-env",{"target": {"ie": 11}, "modules":"cjs"}], ["@babel/preset-react"]],
    "plugins": [["@babel/plugin-transform-runtime",{"corejs":3}]]
}

.babelrc.json

//webpack.config.js - Project wide configuration
module.exports = {
  ...,
  module: {
    rules: [{
        test: /\.(js|jsx)$/,
        exclude: /node_modules\/(?!(axios|@redux-saga|redux-logger))/,
        use: {
          loader: 'babel-loader',
          options: {
            configFile: './apps/envoy/.babelrc',
          },
        },
        }]
    }
}

webpack.config.js

//package.json
{
  "dependencies": {
    "core-js": "^3.6.5",
      ...
    },
  "devDependencies": {
    "@babel/core": "^7.11.5",
    "@babel/plugin-transform-runtime": "^7.11.5",
    "@babel/preset-env": "^7.11.5",
    "@babel/preset-react": "^7.10.4",
    "@babel/runtime-corejs3": "^7.11.2",
    "babel-loader": "^8.1.0",
    ...
  }
}

package.json

global scope가 오염되지 않은 Internet Explore 환경의 테스트 페이지

 

마치며

 

Babel7과 corejs@3를 사용해 적절한 바벨 설정을 하면 웹톡과 같이 고객 페이지에 삽입되는 서비스들도 ES6 최신 문법을 사용하면서 고객 페이지의 전역 스코프도 오염시키지 않는 환경을 만들 수 있습니다. 한 가지 아쉬운 점은 transform-runtime 플러그인의 경우 preset-env와 다르게 target 옵션을 지원하지 않아 브라우저에 맞게 폴리필 최적화를 할 수 없습니다. 이후 transform-runtime 플러그인에서도 target 옵션을 지원한다는 계획이 있으니 추후 업데이트를 확인해서 반영하면 좋겠습니다. 다음에 이어질 FE개발자 세 번째 성장 스토리도 많은 관심 부탁 드립니다.

감사합니다 🙂

 



참고

 

 

카카오톡 공유 보내기 버튼

Latest Posts

제5회 Kakao Tech Meet에 초대합니다!

Kakao Tech Meet #5 트렌드와 경험 및 노하우를 자주, 지속적으로 공유하며 개발자 여러분과 함께 성장을 도모하고 긴밀한 네트워크를 형성하고자 합니다.  다섯 번째

테크밋 다시 달릴 준비!

(TMI: 이 글의 썸네일 이미지는 ChatGPT와 DALL・E로 제작했습니다. 🙂) 안녕하세요, Kakao Tech Meet(이하 테크밋)을 함께 만들어가는 슈크림입니다. 작년 5월에 테크밋을 처음 시작하고,