Back
Featured image of post Polyfill이 무엇인가요??

Polyfill이 무엇인가요??

브라우저 호환성을 위한 polyfill 종류와 사용 방법을 소개합니다.

들어가면서

브라우저 호환성 문제를 해결하기 위한 폴리필(polyfill) 개념과 실제 사용 방법을 조사하였습니다. React와 IE11 환경에서 동작하기 위한 폴리필을 중점적으로 다룹니다.

Polyfill 개념

기본적으로 지원하지 않는 이전 브라우저에서 최신 기능을 제공하는 데 필요한 코드 (JavaScript) ”mdn”

자바스크립트는 끊임없이 발전되어왔기 때문에 명세도 계속 변화되어왔습니다.
이 과정에서 모든 브라우저는 발전된 스크립트 문법을 제공하지 않을 수 있기 때문에 브라우저 호환성 문제를 해결하기 위해 일종의 코드 조각을 추가하는 것을 polyfill이라고 합니다. can-i-use

polyfill을 지원하는 대표적인 라이브러리는 core-jspolyfill.io가 있습니다.
어떻게 사용되는지 한번 소개드리도록 하겠습니다.


Polyfill.io

User-Agent 헤더를 통해 브라우저를 파악하고, 스크립트 실행을 위한 적절한 코드를 제공합니다.
polyfill.io에서는 필요한 기능들에 대한 polyfill을 선택하면, CDN 스크립트를 제공하여 필요한 페이지에서 로드하여 사용할 수 있습니다.

document.addEventListner('DOMContentLoaded', function () {
  fetch('https://jsonplaceholder.typicode.com/posts/1')
    .then((response) => response.json())
    .then((json) => console.log(json));
});

이 코드는 ie에서 정상적으로 실행이 어렵습니다. (화살표 함수, fetch) 02-ie-error ie에서도 정상적으로 최신 문법을 실행시키도록 하려면 별도의 polyfill을 추가시키면 됩니다.
polyfill.io를 활용해 해결하는 방법은 원하는 polyfill이 필요한 페이지에 CDN 스크립트를 삽입하면 됩니다.

<script src="https://polyfill.io/v3/polyfill.min.js?features=es2016%2Cfetch"></script>
<script src="app.js"></script>

polyfill.io에서 제공된 스크립트를 살펴보면, 해당 브라우저에서 지원되지 않는 문법을 자체적으로 사용 가능한 문법으로 감싸주어 사용할 수 있도록 지원해주고 있습니다.

...
//fetch를 사용할 수 있도록 XMLHttpRequest을 활용해 래핑한 함수를 제공한다.
function fetch(input, init) {
    return new Promise(function(resolve, reject) {
      var request = new Request(input, init);

      if (request.signal && request.signal.aborted) {
        return reject(new exports.DOMException('Aborted', 'AbortError'))
      }

      var xhr = new XMLHttpRequest();

      function abortXhr() {
        xhr.abort();
      }

      xhr.onload = function() {
        var options = {
          status: xhr.status,
          statusText: xhr.statusText,
          headers: parseHeaders(xhr.getAllResponseHeaders() || '')
        };
        options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL');
        var body = 'response' in xhr ? xhr.response : xhr.responseText;
        setTimeout(function() {
          resolve(new Response(body, options));
        }, 0);
      };

      ..
      ...
  }

global.fetch = fetch;

('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});

(fetch를 통해 성공적으로 데이터를 가져온 모습) ie-success

핵심 기능

Polyfill.io는 URL 파라미터를 통해 필요한 상세 옵션을 전달할 수 있습니다.

  • feature
    필요한 기능만 선택적으로 요청할 수 있습니다.
    https://polyfill.io/v3/polyfill.min.js?features=Array.from&CArray.isArray&fetch
    
  • falgs
    필요한 폴리필을 모든 브라우저에서 요청하려면 flags에 always 매개변수를 전달하면 됩니다. gated 매개변수를 전달하면 제공받은 브라우저에서 동작하는지 확인하고 필요한 경우에만 폴리필을 제공합니다.
    https://polyfill.io/v3/polyfill.js?features=es5,es6,es7&flags=gated
    
  • unknown
    폴리필을 지원하지 않는 브라우저에서는 기능을 내려줄지 결정해야 합니다. ignore으로 설정하면 지원하지 않는 브라우저에서 폴리필을 제공하지 않고, polyfill을 전달하면 폴리필 코드를 제공합니다.
    https://polyfill.io/v3/polyfill.min.js?features=fetch&unknown=ignore
    

Core.js

Core.js는 babel 환경과의 통합이 잘 되어있어 npm 개발 환경에서 많이 사용하는 polyfill 라이브러리입니다.
Babel이 수행하는 역할에 대해 간단히 살펴보겠습니다.

Babel의 개념

Babel은 ES2015 이상의 문법들을 브라우저에서 호환될 수 있도록 변환해주는 도구입니다. 변환 대상의 범위는 별도의 설정 파일을 이용하여 상세하게 지정할 수 있습니다.

Polyfill과 역할은 유사하지만 차이점은 분명합니다. polyfill은 최신 문법에 대해 지원하지 않는 하위 브라우저를 위한 추가적인 기능 제공을 수행하며, Babel은 ES2015 이상의 문법들을 하위 버전으로 변환해주는 기능을 수행하는데 기능에 따라 변환하지 못 하는 것들도 존재할 수 있습니다.

예를 들면 다음과 같습니다.

  • 새로운 전역 Window에 추가된 객체 (Promise, Map, Set)
  • 새로운 메서드 (Array.from, Array.include)

이렇게 Babel이 제공해주지 못하는 문법을 사용하기 위해서는 별도의 polyfill을 추가해주어야 합니다.

Babel을 활용한 core-js 구성하기

먼저 webpack을 구성합니다. polyfill 수행에 핵심적인 파일은 Babel이기 때문에 webpack에 대해서는 깊게 다루지는 않겠습니다.

React

다음은 웹팩과 바벨을 통한 번들링 및 트랜스파일링이 잘 되는지 확인하기 위해 간단한 리액트 코드를 작성합니다.

//📂src/App.jsx
import React from 'react';
import { useEffect, useState } from 'react';

export const App = () => {
  const names = ['jiny', 'island'];

  const [posts, setPosts] = useState(null);
  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/posts/1')
      .then(function (response) {
        return response.json();
      })
      .then(function (json) {
        console.log(console.log(json));
        setPosts(json);
      })
      .catch(function () {
        alert('assda');
      });
    console.log([1, 2, 3, 4].includes('3'));
  }, []);

  return (
    <div>
      <h2>Posts Fetch</h2>
      <h3>
        title: <span>{posts?.title}</span>
      </h3>
      <p>{posts?.body}</p>
      <ul>
        {names.map((item) => {
          return <li>{item}</li>;
        })}
      </ul>
    </div>
  );
};
//📂src/index.js
import { render } from 'react-dom';
import { App } from './App';

render(<App />, document.querySelector('#root'));

위의 코드를 살펴보면 ES2015 이상의 const, let, Array.map 등의 코드로 이루어져 있습니다.
이를 구형 브라우저에서 인식하기 위해서는 Babel을 사용하여 트랜스파일링이 필요합니다.

Babel 설정

Babel에서 core-js를 사용하려면 @babel/preset-env 프리셋을 적용해야 합니다.
이전에는@babel/polyfill 프리셋을 사용했지만 7.4.0 버전 이후로는 deprecated되었습니다.

@babel/polyfill

글로벌 파일에 @babel/polyfill을 직접 import하여 사용했던 방식이었습니다.
core-js를 단순하게 import하기 때문에 전역 환경을 오염시키는 문제가 존재하였습니다. (babel-polyfill)

import '@babel/polyfill';
// Cover all standardized ES6 APIs.
import 'core-js/es6';

// Standard now
import 'core-js/fn/array/includes';
import 'core-js/fn/array/flat-map';
import 'core-js/fn/string/pad-start';
import 'core-js/fn/string/pad-end';
import 'core-js/fn/string/trim-start';
import 'core-js/fn/string/trim-end';
import 'core-js/fn/symbol/async-iterator';
import 'core-js/fn/object/get-own-property-descriptors';
import 'core-js/fn/object/values';
import 'core-js/fn/object/entries';
import 'core-js/fn/promise/finally';

// Ensure that we polyfill ES6 compat for anything web-related, if it exists.
import 'core-js/web';

import 'regenerator-runtime/runtime';

@babel/preset-env
core-jstargets 옵션을 사용하여 상세한 타깃 브라우저를 설정할 수 있으며, polyfill 변환 기능을 제공하는 플러그인 입니다.

  • corejs: 사용하려는 core-js 라이브러리 버전을 타깃합니다. (useBuitIns 옵션이 활성화 될 때 적용)

  • useBiuiltIns

    • entry: core-js 모듈을 직접 import하여 사용하는 방법입니다.

      import "core-js/stable/array";
      
      const fruits = ["사과", "바나나"];
      fruits.forEach((item) => {
        console.log(item);
      });
      
      const apple = fruits.filter((item) => item === "사과");
      
      //transfile
      ("use strict");
      
      require("core-js/modules/es.array.concat.js");
      require("core-js/modules/es.array.copy-within.js");
      require("core-js/modules/es.array.fill.js");
      require("core-js/modules/es.array.filter.js");
      require("core-js/modules/es.array.find.js");
      require("core-js/modules/es.array.find-index.js");
      ...
      require("core-js/modules/es.array.unscopables.flat-map.js");
      require("core-js/modules/es.object.to-string.js");
      require("core-js/modules/es.string.iterator.js");
      
      var fruits = ["사과", "바나나"];
      fruits.forEach(function (item) {
        console.log(item);
      });
      var apple = fruits.filter(function (item) {
        return item === "사과";
      });
      
    • usage: 코드에 필요한 polyfill 구문만 자동으로 삽입되도록 하는 방식입니다.

      //example
      const fruits = ['사과', '바나나'];
      fruits.forEach((item) => {
        console.log(item);
      });
      
      const apple = fruits.filter((item) => item === '사과');
      
      //transfile
      ('use strict');
      
      require('core-js/modules/es.object.to-string.js');
      require('core-js/modules/es.array.filter.js');
      var fruits = ['사과', '바나나'];
      fruits.forEach(function (item) {
        console.log(item);
      });
      var apple = fruits.filter(function (item) {
        return item === '사과';
      });
      
  • targets: 타깃 browserlist 옵션을 따로 지정할 수 있습니다. browserlist

    //지원 타겟팅 환경: android, chrome, deno, edge, electron, firefox, ie, ios, node, opera, rhino, safari, samsung.
    {
      ...
      "targets": { //특정 브라우저 또는 런타임 환경 버전에 맞춰 타겟팅
        "chrome": "58",
        "ie": "11"
      }
    }
    
    {
      ...
      "targets": { //특정 지역의 브라우저 사용율, 버전에 맞춰 타겟팅
        "browsers": [" > 5% in KR", "last 2 Chrome version"]
      }
    }
    

@babel/preset-env로 구성한 최종 설정

//.babelrc.json
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "corejs": "3.29",
        "useBuiltIns": "entry",
        "targets": {
          "ie": "11"
        }
      }
    ],
    "@babel/preset-react" //react 환경을 위한 babel preset
  ],
  "plugins": []
}

@babel/plugin-transform-runtime
@babel/runtimecore-js3가 통합된 transform-runtime 플러그인을 사용하면 전역 스코프를 오염시키지 않습니다. 또한, 기존 @babel/runtime에서 지원하지 않았던 인스턴스 메서드도 지원합니다.

var _interopRequireDefault = require('@babel/runtime-corejs3/helpers/interopRequireDefault');
var _forEach = _interopRequireDefault(require('@babel/runtime-corejs3/core-js-stable/instance/for-each'));
var _filter = _interopRequireDefault(require('@babel/runtime-corejs3/core-js-stable/instance/filter'));

var fruits = ['사과', '바나나'];
(0, _forEach.default)(fruits).call(fruits, function (item) {
  console.log(item);
});
var apple = (0, _filter.default)(fruits).call(fruits, function (item) {
  return item === '사과';
});

corejs 속성으로 런타임 헬퍼를 지정할 수 있습니다.

  • fasle (기본): babel/runtime
  • 2: @babel/runtime-corejs2
  • 3: babel/runtime-corejs3
//.babelrc.json
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "ie": 11
        },
        "modules": "cjs"
      }
    ],
    "@babel/preset-react"
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 3
      }
    ]
  ]
}

최종적으로 @babel/plugin-transform-runtime 설정을 통해 ie11 환경에서도 react 페이지를 정상적으로 실행시킬 수 있었습니다. fetch 문법은 ECMA Script 명시된 문법은 아니므로, 별도의 polyfill 추가가 필요합니다.

success-react-ie


유용한 Polyfill

whatwg-fetch

fetch를 제공해주는 polyfill 입니다.

//📁src/index.js
import 'whatwg-fetch'

fetch(...);

react-app-polyfill

ie 브라우저에 초점을 맞춘 react에서 제공하는 polyfill 입니다.
아래 5가지 polyfill을 제공합니다.

  • promise, async-await
  • fetch
  • Object.assign
  • Symbol
  • Array.from, Array Spread
//📁src/index.js
import 'react-app-polyfill/ie9';
import 'react-app-polyfill/stable';

참고 자료