[JS] 구형 엔진에서 최신 문법을 사용하기: 트랜스파일과 폴리필

언제까지나 구 문법으로 코딩할 수 없으니까


Table Of Contents


TL;DR


JavaScript는 하위 호환성은 보장하지만, 상위 호환성을 보장하지는 않는다. 따라서 새로운 문법이 이전 실행 환경에서 작동하지 않을 수 있다.

이런 문제를 해결하기 위해서, 우리는 트랜스파일폴리필이라는 방법을 사용할 수 있다.

1. JS와 하위 호환성


  • JavaScript는 하위 호환성(backwards compatibility) 을 보장하는 언어다.
    • 단 한 번이라도 유효한 JS 문법이라고 인정되면 절대 그 유효성이 깨지지 않는다.
    • 따라서 JS가 만들어진 1995년에 작성된 코드를 지금 실행시켜도 무조건 작동함이 보장된다.
  • 예외적인 상황도 있다.
    • TC39(JS를 유지하고 발전시키는 국제 표준화 기구)에서 하위 호환성을 깨는 결정을 하기도 했다.
    • 예를 들어, ES5에서는 Strict Mode(엄격 모드) 가 도입되었는데, 엄격 모드에서는 기존 동작이 작동하지 않을 수 있다.
      • 엄격 모드에서 arguments.callee 속성을 사용할 경우, TypeError를 받는다.
      • mdn - arguments.callee에서 deprecated되었는지에 대한 설명을 읽어볼 수 있다.
  • 하지만 상위 호환성(forwards compatibility) 을 보장하지는 않는다.
    • 상위 호환성을 준수한다면 새로 추가된 문법을 이용해 코드를 작성하고, 이전 JS 엔진에서 해당 코드를 작동할 때 문제가 발생하지 않아야 한다.
    • cf) HTML, CSS는 상위 호환성을 보장하고, 하위 호환성을 보장하지 않는다.

2. 트랜스파일


JS는 상위 호환성을 보장하지 않기 때문에, 이전에 개발된 엔진에서는 최신 문법으로 작성된 코드가 작동하지 않을 수도 있다.

이런 경우, 최신 문법으로 작성된 JS 코드를 이전 버전의 JS 코드로 변환하는 과정을 거치면 구형 엔진에서도 코드를 작동시킬 수 있다. 이 과정을 트랜스파일이라고 한다.

트랜스파일 vs 컴파일

위에서 트랜스파일이라는 개념을 언급했는데, 트랜스파일은 컴파일과 자주 함께 언급된다.

컴파일(Compile)

컴파일은 주어진 언어로 작성된 컴퓨터 프로그램을 다른 언어의 동등한 프로그램으로 변환하는 과정이다. 이런 작업을 실행하는 소프트웨어를 컴파일러라고 부른다.

  • 예를 들어, C++, Rust, Java 등의 고급 언어로 작성된 코드를 binary 코드로 변환하는 일 등이 있다.
  • 또한 TypeScript에서 JavaScript처럼 고급 언어에서 고급 언어로도 변환할 수 있는데, 이런 경우에는 트랜스파일이라고도 부른다.

트랜스파일(Transpile)

한 언어로 작성된 프로그램을 비슷한 수준의 추상화를 가진 언어로 변환하는 경우를 특별히 트랜스파일이라고 부른다.

  • 이번에 설명한 최신 JS 문법을 이전 문법의 JS 코드로 변환하는 일은 컴파일 중에서도 트랜스파일에 속한다고 할 수 있다.
  • 위에서 말한 TS에서 JS로의 변환 또한 두 언어 모두 고급 언어이기 때문에, 트랜스파일이라고 부를 수 있다.

예시

ES6+ 문법(let, const, arrow funtion 등)을 구 문법으로 변환한다.

// arrow function을 사용함 [1, 2, 3].map((n) => n + 1);
// 함수 표현식을 사용함 [1, 2, 3].map(function (n) { return n + 1; });

Babel

가장 대표적인 트랜스파일러로 Babel이 있다.

Babel은 최신 JS 문법을 구형 브라우저나 엔진에서 작동 가능하도록 변환한다. 또한, 이후에 설명할 폴리필을 통해 구형 엔진에서 작동하지 않는 API가 작동하도록 만들어준다.

폴리필(polyfill) / 심(Shim)


최신 JS API가 구형 브라우저에서 지원되지 않는 경우, 해당 기능을 사용할 수 있도록 코드 조각 또는 라이브러리를 제공하는 방식이다.

위에서도 언급했지만, Babel같은 트랜스파일러는 폴리필이 필요한 코드를 찾아내고, 자동으로 폴리필을 추가해준다.

예시

아래 예시는 You Don't Know JS Yet에서 가져온 예시다.

ES2019의 Promise 프로토타입에 추가된 finally() 메서드를 사용한다. 만약 ES2019를 지원하지 않는 환경에서 이 코드를 실행하면 finally()가 존재하지 않으므로 오류가 발생한다.

// getSomeRecords()는 원격으로 가져온 데이터를 담은 promise를 반환합니다. var pr = getSomeRecords(); // 데이터를 가져오는 동안 화면에 스피너(spinner)를 보여줍니다. startSpinner(); promise .then(renderRecords) // 가져온 데이터를 화면에 표시합니다 .catch(showError) // 데이터를 가져오는 데 실패했다면 오류가 발생합니다. .finally(hideSpinner); // 성공과 실패 여부와는 상관없이 마지막에는 스피너를 숨깁니다.

이럴 때는 Promise.prototype.finally 메서드가 존재하는지 확인하고, 존재하지 않는다면 아래와 같이 폴리필을 제공해 주면 된다.

if (!Promise.prototype.finally) { Promise.prototype.finally = function f(fn) { return this.then( function f(v) { ... }, function c(e) { ... } ) } }

혹은 함수를 미리 정의하고, 아래와 같이 사용할 수도 있다.

Promise.prototype.finally = Promise.prototype.finally ?? /* Promise.prototype.finally 함수 구현 */

core-js

표준적으로 사용하는 플리필들은 https://github.com/zloirock/core-js 에 정의되어 있으니 이 코드를 import해서 사용할 수 있다.

import "core-js/actual";

처럼 import하면 모든 폴리필들을 로드하게 된다.

만약 틀정 플리필만 로드하고 싶다면,

import "core-js/actual/promise"; import "core-js/actual/set"; import "core-js/actual/iterator"; import "core-js/actual/array/from"; import "core-js/actual/array/flat-map"; import "core-js/actual/structured-clone";

처럼 사용할 수도 있다.

단점

이렇게 구형 브라우저에서 필요한 기능을 사용하기 위해서 폴리필이 필요하지만, 아쉽게도 단점도 따른다.

우선 폴리필은 공식적인 기능이 아니라 표준을 모방한 것이기 때문에, 표준과 100% 동일하지 않을 수도 있다. 따라서 폴리필을 적용한 기능이 브라우저마다 다르게 동작할 수 있다.

또한, 최신 브라우저에서는 이미 지원하는 기능을 포함할 수 있기 때문에, 불필요한 코드를 받아야 하고, 따라서 번들 크기가 커지면서 전송 속도가 느려질 수 있다. 위에서 본 import "core-js/actual";를 사용할 때 이 문제가 두드러진다.

  • 성능 문제를 해결하기 위해서는 polyfill.io라는 서비스를 주로 사용하는 듯 한데, 서비스가 사라졌다...?
  • Babel에 @babel/preset-env를 설정해 필요한 폴리필만 불러올 수 있다.
    module.exports = { presets: [ ['@babel/preset-env', { targets: { /* 원하는 타겟의 목록 */ } }], ], ... };
    • 심지어 @babel/preset-env.browserslistrc를 함께 사용하면, 시장 점유율이 특정 비율 이상인 브라우저에 필요한 폴리필만 로드할 수도 있다! 자세한 내용은 Browserslist Integration을 참고하자.
    • 토스 블로그에서 폴리필 시스템을 자체적으로 제작한 사례를 보여주고 있으니 참고하면 좋을 것 같다!

참고