프로필
프론트엔드 개발자
김동희입니다

react-scripts 에서 Promise.any polyfill 추가하기

I. 문제점

iOS 13 버젼의 기기에서 Promise.any is not a function. TypeError가 발생하여 Promise.any를 사용하는 기능들이 동작하지 않는 이슈가 발생하였다.

react-app-polyfill을 import 하고 있고, .browerslistrc 에 iOS 11 이상도 명시해두었기에 Promise.any의 polyfill도 자동으로 추가될 것이라 생각하였으나, 실제로는 추가되지 않은 것으로 보여졌다.

현황

// package.json
"dependencies": {
  "react-scripts": "^5.0.0",
  "react-app-polyfill": "^3.0.0",
}
// index.tsx
import "react-app-polyfill";

Promise.any의 지원범위

image

II. 해결책을 찾기 위한 접근방법

1. react-app-polyfill 의 동작방법 조사

react-app-polyfillcore-js/stableruntime-generator 를 import 하는 것이 동작의 전부이다.

runtime-generator는 iterator-iterable 프로토콜과 관련된 polyfill 이므로, Promise.any 와는 전혀 관련이 없다.

core-js/stablecore-js 에 존재하는 수많은 polyfill 중 proposals를 제외한 stable한 기능의 polyfill만을 추가한다. 어떤 기능이 stable한 것인지는 core-js 버젼에 따라 상이하며, 링크에서 core-js 버젼별 stable한 기능을 확인할 수 있다.

2. @babel/preset-env 동작방법 조사

babel은 트랜스파일 과정에서 @babel/preset-env 을 이용하여 기존의 코드를 target 환경에서 동작하는 구문으로 변경하는데, target 환경이 지원하지 않는 JS 최신 문법이라면 polyfill을 추가할 수 있다. 단, core-js를 import하는 것이 필수적이다.

@babel/preset-env의 옵션 중 useBuilltIns@babel/preset-env 가 어떻게 polyfill을 다룰 것인지 조정하는 옵션으로, "usage" | "entry" | false 의 값을 가질 수 있으며 default 값은 false이다. false 인 경우에는 polyfill을 추가하지 않는다.

useBuiltIns: "usage"

"usage"로 설정한 경우, 기존의 코드에서 사용중인 기능을 target 환경에서 지원하지 않는다면 해당 기능에 대한 polyfill만 트랜스파일 시 추가한다.

// 트랜스파일 전
const m = new Map();
// 트랜스파일 후

// target 환경이 Map을 지원하지 않는 경우
import "core-js/modules/es.map";

const m = new Map();

// target 환경이 Map을 지원하는 경우
const m = new Map();

useBuiltIns: "entry"

"entry" 로 설정한 경우, 기존의 코드내에서 어떤 기능을 사용하는지 상관없이 target 환경이 지원하지 않는 모든 기능의 polyfill을 추가한다. 이 때 @babel/preset-envimport "core-js/stable"; 구문을 target 환경이 지원하지 않는 기능에 대한 polyfill 각각의 모듈 import로 변경한다.

// 트랜스파일 전
import "core-js/stable";
// 트랜스파일 후
import "core-js/modules/es.string.pad-start";
import "core-js/modules/es.string.pad-end";
// ...

target 환경이 지원하지 않는 기능을 판단하기 위해 @babel/preset-env"corejs" 로 버젼을 지정할 수 있다. 실제로 설치된 core-js 버젼과 무관하게, 지정해준 corejs 버젼에 의해 어떤 polyfill의 import로 변경되는지가 결정된다.

https://babeljs.io/docs/en/babel-preset-env#usebuiltins-entry

https://babeljs.io/docs/en/babel-preset-env#corejs

3. babel-preset-react-app 동작방법 조사

react-scripts 는 내부적으로 babel-preset-react-app 이라는 라이브러리를 사용해서 babel 의 각종 preset을 설정하고 있다.

babel-preset-react-app 은 위에서 언급한 @babel/preset-env 외에도 @babel/preset-react, @babel/preset-typescript 등 각종 preset을 처리한다.

III. 문제의 원인 파악

babel-preset-react-app@babel/preset-env를 설정할 때 다음과 같은 옵션값을 주고 있다.

{
  presets: [
    // Latest stable ECMAScript features
    require("@babel/preset-env").default,
    {
      // Allow importing core-js in entrypoint and use browserlist to select polyfills
      useBuiltIns: "entry",
      // Set the corejs version we are using to avoid warnings in console
      corejs: 3,
      // Exclude transforms that make all code slower
      exclude: ["transform-typeof-symbol"],
    },
    // ...
  ];
}

@babel/preset-env 의 옵션으로 useBuiltins에는 "entry"로, corejs 는 3을 할당하고 있다.

따라서 import "react-app-polyfill" 에 의해 import 되는 "core-js/stable"을 각각의 개별적인 polyfill 모듈으로 변환 할 때 corejs 3.0.0 을 기준으로 어떤 기능이 stable한지를 판단한다. (3 은 문자열로 변환시 "3" 이고 이는 semver 기준 "3.0.0"을 의미함)

corejs 3.7부터 Promise.any는 stable한 기능에 포함되기 때문에 Promise.any는 proposals에 해당하여 Promise.any에 대한 polyfill 은 추가되지 않았던 것이다.

https://github.com/zloirock/core-js/blob/master/packages/core-js-compat/src/modules-by-versions.mjs)

IV. 해결책

react-app-rewiredreact-scriptswebpack 설정을 변경할 수 있지만, react-scripts의 직접적인 webpack 설정이 아닌 react-scripts의 내부에서 사용되는 babel-preset-react-app 의 설정을 변경하여야 하므로, 불가능한 건 아니지만 매우 번거롭고 현실적으로 어렵다고 볼 수 있다.

따라서 core-js를 별도로 설치한 후에 Promise.any의 polyfill을 명시적으로 import 해준다.

import "core-js/proposals/promise-any";
import "react-app-polyfill";

core-js 3.0.0 기준 Promise.any는 proposals 이므로, "core-js/proposals/promise-any"를 import 한다.