Polyfill

Taehun Jeong·2023년 2월 28일
0
post-thumbnail
post-custom-banner

ECMAScript

자바스크립트는 매년 다양한 기능을 개발하고 이를 지원한다. 정보 통신에 대한 표준을 관리하는 비영리기구 Ecma International에서 새로운 규격을 제작하여 ES6를 기점으로 매년마다 발표하고 있다. 자바스크립트는 ECMAScript를 준수하는 언어이므로 ECMAScript의 규격 변화에 따라 Javascript의 성능도 변화한다고 할 수 있다.


옛날 브라우저에서는?

브라우저 별로 지원사항을 확인할 수 있는 ECMAScript Table
키워드 별로 지원사항을 확인할 수 있는 CanIUse

오래된 브라우저에서는 최신 기술사항을 지원하지 않는다. 위의 페이지에서는 브라우저, 키워드에 따라 브라우저 별로 지원되는 기술사항을 확인할 수 있다. 예를 들어 2015년의 ES6 이상 버전의 let, const, 화살표 함수, promise 등은 internet explorer 등에서 더 이상 지원하지 않는다. 이를 위해 구형의 브라우저에서도 코드가 작동할 수 있도록 사용 가능한 코드 조각이나 플러그인으로 변경하는 것을 Polyfill이라 한다.


Babel

Babel은 ES6 이상의 코드를 구 브라우저에서도 호환 가능하도록 변경해주는 javascript 컴파일러다. Babel은 다음과 같은 순서로 동작한다.

  1. 파싱: 입력된 javascript 코드를 추상 구문 트리(Abstract Syntax Tree: AST)로 변환한다.
  2. 변환: AST를 탐색하며 새로운 코드를 생성하거나 기존의 코드를 변경한다. 이 단계에서 사용자가 지정한 babel plugin들을 사용한다.
  3. 생성: AST를 다시 javascript 코드로 변경한다. 이 단계에서 babel이 수정한 코드가 출력된다.

Babel의 사용법에 대해 알아보자.

먼저, 프로젝트에 babel을 설치한다.

npm install --save-dev @babel/core @babel/cli @babel/preset-env

babel의 설정 파일 .babelrc을 생성하고 적용할 preset과 플러그인을 정의한다.

{
  "presets": [
    "@babel/preset-env"
  ]
}

예를 들어, internet explorer 11을 지원해야 한다면 다음과 같이 작성한다.

module.exports = {
  presets: [
    ['@babel/preset-env', { targets: { ie: 11 } }],
  ],
  /* 그 외의 설정 */
};

babel은 명령어로 실행하거나 프로젝트 빌드 시에 자동으로 적용되도록 설정할 수 있다.
명령어로 실행하려면

npx babel [입력 파일 경로] -o [출력 파일 경로]

형태로 입력한다.

빌드 시 자동으로 적용되도록 하려면, 예를 들어 vite의 경우 vite.config.js에 다음과 같이 플러그인을 추가한다.

import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    {
      name: 'babel',
      transform(code, id) {
        if (/\.js$/.test(id)) {
          const transformed = require('@babel/core').transform(code, {
            presets: ['@babel/preset-env']
          });
          return {
            code: transformed.code,
            map: transformed.map
          };
        }
      }
    }
  ]
});

Mini-Mission: Array.from의 polyfill 만들기

Array.from은 유사 배열 객체나 반복 가능한 객체에 대하여 shallow copy를 통해 새로운 array 객체를 생성하는 메서드이다. ES6에서 처음 등장한 문법으로 이전의 브라우저에서도 작동할 수 있도록 polyfill을 만들어보자.

if (!Array.from) {
  Array.from = function(object) {
    'use strict';

    if (object == null) {
      throw new TypeError('Array.from requires an array-like object - not null or undefined');
    }

    var arrayLikeObject = Object(object);

    if (arguments.length > 1) {
      var mapFunction = arguments[1];
      var thisArg = arguments[2];
    }

    var length = Number(arrayLikeObject.length);

    var newArray = new Array(length);

    for (var i = 0; i < length; i++) {
      var value = arrayLikeObject[i];

      if (mapFunction) {
        newArray[i] = typeof thisArg === 'undefined' ? mapFunction(value, i) : mapFunction.call(thisArg, value, i);
      } else {
        newArray[i] = value;
      }
    }

    return newArray;
  };
}

참고로 Array.from이 지원되지 않으면 let, const도 지원되지 않으므로 모든 변수 선언은 var을 사용한다...
코드에 대해 설명을 덧붙이면,

  • if (!Array.from)을 통해 브라우저에서 Array.from을 지원하지 않을 때에만 Polyfill을 지원한다.
  • 입력으로 전달받은 object가 null이거나 undefined일 경우에는 TypeError 예외를 발생시킨다.
  • Obejct(object)를 사용해 입력으로 전달받은 object를 배열 형태로 변환하여 arrayLikeObject에 저장한다.
  • argumnets 객체의 길이가 1보다 큰 경우, 즉 배열의 각 값마다 적용할 메서드를 함께 전달받은 경우에는 적용할 메서드 mapFunction과 사용할 객체 thisArg를 설정한다.
  • length에는 앞서 생성한 배열 형태의 object 객체의 길이를 저장한다. newArray의 길이는 length를 갖는다.
  • for문을 통해 arrayLikeObject의 각 요소를 newArray에 복사한다. 이때, mapFunction이 지정된 경우, 각 요소마다 mapFunction을 호출하여 반환된 값을 저장한다.
  • 생성된 Array 객체 newArray를 반환한다.

아래는 해당 코드를 작성하는 데 참조한 mdn의 Array.from의 polyfill이다.

/**
 * Array.from() polyfill
 */
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from
// Production steps of ECMA-262, Edition 6, 22.1.2.1
if (!Array.from) {
	Array.from = (function () {
		var toStr = Object.prototype.toString;
		var isCallable = function (fn) {
			return typeof fn === 'function' || toStr.call(fn) === '[object Function]';
		};
		var toInteger = function (value) {
			var number = Number(value);
			if (isNaN(number)) { return 0; }
			if (number === 0 || !isFinite(number)) { return number; }
			return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number));
		};
		var maxSafeInteger = Math.pow(2, 53) - 1;
		var toLength = function (value) {
			var len = toInteger(value);
			return Math.min(Math.max(len, 0), maxSafeInteger);
		};

		// The length property of the from method is 1.
		return function from(arrayLike/*, mapFn, thisArg */) {
			// 1. Let C be the this value.
			var C = this;

			// 2. Let items be ToObject(arrayLike).
			var items = Object(arrayLike);

			// 3. ReturnIfAbrupt(items).
			if (arrayLike == null) {
				throw new TypeError('Array.from requires an array-like object - not null or undefined');
			}

			// 4. If mapfn is undefined, then let mapping be false.
			var mapFn = arguments.length > 1 ? arguments[1] : void undefined;
			var T;
			if (typeof mapFn !== 'undefined') {
				// 5. else
				// 5. a If IsCallable(mapfn) is false, throw a TypeError exception.
				if (!isCallable(mapFn)) {
					throw new TypeError('Array.from: when provided, the second argument must be a function');
				}

				// 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined.
				if (arguments.length > 2) {
					T = arguments[2];
				}
			}

			// 10. Let lenValue be Get(items, "length").
			// 11. Let len be ToLength(lenValue).
			var len = toLength(items.length);

			// 13. If IsConstructor(C) is true, then
			// 13. a. Let A be the result of calling the [[Construct]] internal method
			// of C with an argument list containing the single item len.
			// 14. a. Else, Let A be ArrayCreate(len).
			var A = isCallable(C) ? Object(new C(len)) : new Array(len);

			// 16. Let k be 0.
			var k = 0;
			// 17. Repeat, while k < len… (also steps a - h)
			var kValue;
			while (k < len) {
				kValue = items[k];
				if (mapFn) {
					A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k);
				} else {
					A[k] = kValue;
				}
				k += 1;
			}
			// 18. Let putStatus be Put(A, "length", len, true).
			A.length = len;
			// 20. Return A.
			return A;
		};
	}());
}

References

tosstech) 똑똑하게 브라우저 Polyfill 관리하기
요즘IT) ECMAScript 2022 살펴보기
sanha.log) JS ES6 - Polyfill
mdn web docs) Polyfill
2ssue's dev note) Array.from 이 모든 브라우저에서 동작하도록 polyfill 코드를 만들어보세요

profile
안녕하세요
post-custom-banner

0개의 댓글