본문 바로가기
WEB/깊게 공부하기

[JS] generator polyfill regenerator 살펴보기 - Generator 3

by IT황구 2023. 10. 30.
728x90
반응형

[JS] generator 살펴보기 - Generator 2

 

[JS] generator 살펴보기 - Generator 2

Generator 1편과 이어서 보신다면 더 좋습니다. https://rbals0445.tistory.com/142 [JS] iterable, iterator 살펴보기 - Generator 1 안녕하세요! 오랜만에 기술 포스트를 작성하네요. 이번 글은 시리즈처럼 작성될 예

rbals0445.tistory.com

이전 글과 이어서 보신다면 이해하기 더 좋습니다.

 

지난 글에서 generator에 대해서 알아봤습니다.

함수의 실행을 중간에 멈추고 이어서 다시 실행하려면 어떻게 코드를 만들어야 할까요?

 

Babel에서 generator를 트랜스파일 하면 generator가 사용되지 않은 코드가 나옵니다.

10줄도 안되게 입력했는데, 500줄에 달하는 코드가 생성됩니다. 숨이 턱턱 막히지만 이 부분에 대해서 알아봅니다.

 

Babel은 regenerator를 사용하고 있습니다. 이 코드에 대해서 알아보도록 하겠습니다.

 

 

regenerator란

"generator, yield를 작동하게 만들어주는 polyfill libarary"

 

facebook의 팀원이 미래에 도입될 generator의 기능을 기다리던 중 ES5 환경에서도 동작하게 개발한 것이 바로 'regenerator' 라이브러리입니다.

 

https://facebook.github.io/regenerator/

 

Regenerator

Let’s get serious about ES6 generator functions. Several months have passed since support for generator functions and the yield keyword arrived in Node.js v0.11.2. This news was greeted with great excitement, because generator syntax provides a much clea

facebook.github.io

 

Babel로 코드를 변환하게 되면 minify가 되어 나오므로 어떤 내용이 있는지 알아보기 어렵습니다.

해당 코드에 대한 내용은 아래 링크에서 자세히 확인할 수 있습니다.

 

https://github.com/facebook/regenerator/blob/main/packages/runtime/runtime.js

 

변환 결과 훑어보기

지금부터 숨을 참아야 합니다. 처음에 변환된 코드를 보고 과호흡으로 쓰러질 뻔했습니다. 

Babel의 변환 결과를 먼저 확인하고, 느낌을 알아보도록 하겠습니다.

 

Before

function *generator(){
  yield 1;
  return 4;
}

const gen = generator();

console.log(gen.next().value)
console.log(gen.next().value)
console.log(gen.next().value)

Before의 코드는 generator()를 통해서 iterator를 반환받고,  값을 하나씩 찍어나가는 코드입니다.

 

 

After

var _marked = /*#__PURE__*/ _regeneratorRuntime().mark(generator);
function generator() {
  return _regeneratorRuntime().wrap(function generator$(_context) {
    while (1)
      switch ((_context.prev = _context.next)) {
        case 0:
          _context.next = 2;
          return 1;
        case 2:
          return _context.abrupt("return", 4);
        case 3:
        case "end":
          return _context.stop();
      }
  }, _marked);
}
var gen = generator();

 

천천히 보도록 하겠습니다.

 

> var _marked = /*#__PURE__*/ _regeneratorRuntime().mark(generator);

  • _regeneratorRuntime 이라는 어떤 함수가 있습니다. 그리고 이 함수 안에는 mark 라는 함수를 반환한다고 생각할 수 있습니다.
  • generator 함수에 어떤 마킹을 하나 봅니다.

 

> return _regeneratorRuntime().wrap(function generator$(_context) {

  • _regeneratorRuntime 함수는 wrap 이라는 함수도 가지고 있는 것을 확인할 수 있습니다.
  • 함수의 이름처럼 내부에 어떤 함수를 감싸고 있습니다.
  • _context 라는 매개변수를 받는데, 이 _context 안에는 generator가 어디를 실행해야 하는지, 이전에 어떤 상태였는지 같은 값들을 가지고 있습니다.

_regeneratorRuntime 이라는 함수 안에서 여러 함수들이 반환되는 구조인 것 같습니다.

 

그리고 중간에 멈췄다가 실행하는 방법을 while(true) prev, next 같은 변수를 이용해서 기억하는 방식으로 변경되었다는 것을 짐작할 수 있습니다.

 

 

소스코드 분석하기

변환된 코드가 시작되면 _regeneratorRuntime 함수가 먼저 호출되게 됩니다. 

 

실행 하자마자 일어나는 일, mark, wrap, makeInvokeMethod, Context 에 대해서 알아보겠습니다.

 

https://github.com/facebook/regenerator/blob/main/packages/runtime/runtime.js#L8

  var Op = Object.prototype;
  var hasOwn = Op.hasOwnProperty;
  var defineProperty = Object.defineProperty || function (obj, key, desc) { obj[key] = desc.value; };
  var undefined; // More compressible than void 0.
  var $Symbol = typeof Symbol === "function" ? Symbol : {};
  var iteratorSymbol = $Symbol.iterator || "@@iterator";
  var asyncIteratorSymbol = $Symbol.asyncIterator || "@@asyncIterator";
  var toStringTagSymbol = $Symbol.toStringTag || "@@toStringTag";

  function define(obj, key, value) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true
    });
    return obj[key];
  }
  try {
    // IE 8 has a broken Object.defineProperty that only works on DOM objects.
    define({}, "");
  } catch (err) {
    define = function(obj, key, value) {
      return obj[key] = value;
    };
  }

  function wrap(innerFn, outerFn, self, tryLocsList) {
    // If outerFn provided and outerFn.prototype is a Generator, then outerFn.prototype instanceof Generator.
    var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator;
    var generator = Object.create(protoGenerator.prototype);
    var context = new Context(tryLocsList || []);

    // The ._invoke method unifies the implementations of the .next,
    // .throw, and .return methods.
    defineProperty(generator, "_invoke", { value: makeInvokeMethod(innerFn, self, context) });

    return generator;
  }
  
  // 후략

 

함수가 실행되자마자 초기화 및 실행이 되는 내용입니다.

 

  1. mark, wrap 등 여러 함수들을 초기화합니다. 이후 사용할 수 있게 export 합니다.
  2. Object.defineProperty, @@iterator 같은 함수나 심볼들의 Polyfill을 정의합니다.
  3. polyfill generator에 next, return 같은 함수들을 등록해 줍니다. 호출은 가능하지만 호출 시 오류가 발생합니다. (this._invoke를 모르기 때문입니다)
    • next, throw, return이 호출되면 this._invoke 를 호출하게 설정되어 있습니다.

   4. 브라우저의 generator와 유사한 프로토타입을 가지도록 세팅하는 작업을 합니다.

  • 브라우저 generator

  • polyfill generator

 

  • 유사
    • next, return, throw 같은 함수들도 모두 있습니다.
    • 심볼도 모두 같게 정의되어 있습니다.
  • 차이
    • polyfill generator에는 _invoke 라는 함수가 있습니다. 역할은 추후 확인 가능합니다.
    • 브라우저 generator에 [[GeneratorState]] 라는 internal slot이 존재합니다. 이 값은 현재 제너레이터가 끝난 상태인지, 진행 중인지, 시작하지 않았는지 이런 내용들을 담고 있습니다. 하지만 polyfill은 이런 게 없죠? 하지만 클로저를 이용해 변수의 값을 기억하게 됩니다.

 

_regeneratorRuntime을 실행만 했을 뿐인데, 많은 내용들이 진행되었습니다.

하지만 코드 사용을 위해서 mark, wrap 같은 함수를 실행해야 합니다. 이 내용들에 대해서 보겠습니다.

 

mark

generator 함수를 사용하기 위해 가장 먼저 호출되는 함수입니다. 맨 처음 훑어보기 목차에서 뭔가 함수에 마킹을 해준다고 했습니다.

 

내가 사용할 generator에 미리 브라우저와 유사하게 만든 generator prototype을 상속 시켜줍니다. 

exports.mark = function(genFun) {
      if (Object.setPrototypeOf) {
        Object.setPrototypeOf(genFun, GeneratorFunctionPrototype);
      } else {
        genFun.__proto__ = GeneratorFunctionPrototype;
        define(genFun, toStringTagSymbol, "GeneratorFunction");
      }
      genFun.prototype = Object.create(Gp);
      return genFun;
    };

 

매개변수 설명

  • genFun
    • 내가 등록한 제너레이터 함수

 

기능

  • _regeneratogeneratorRuntime() 가 실행될때 초기값으로 만들어진 generator polyfill의 기능을 Object.create를 사용해 상속받게 만듭니다.
  • 반환된 genFun은 genFun.next(), genFun.return() 같은 함수가 등록됩니다.
    • 하지만 wrap 함수를 호출하기 전 까지는 next()를 호출해도 에러가 발생합니다.
    • _regeneratorRuntime() 실행시에 next를 호출하면 _this.invoke 가 실행되게 설정되어 있습니다. 하지만 this._invoke는 등록한적이 없습니다.

 

  • genFun.prototype = Object.create(Gp)
    • 여기가 제일 중요한 코드라고 생각합니다. Gp는 아래와 같은 형태를 가지고 있습니다.

Object.create는 객체를 만드는 역할 뿐만 아니라, 내가 만들 객체의 prototype을 지정해줄 수 있습니다. 즉 상속처럼 쓸 수 있습니다.

 

genFun.prototype의 prototype을 Gp로 설정한다는 뜻입니다. 조금 이해하기 어려울 수 있습니다.

 

위 과정을 거치면 genFun은 prototype chain을 이용해서 genFun.next(), genFun.return()에 접근할 수 있게 됩니다.

 

그럼 여기서 의문이 들게 됩니다.

 

왜 genFun이 아니고 genFun.prototype 일까요?

 

이 부분에 대한 답은 스펙문서에서 찾을 수 있었습니다.

스펙문서에 generator 함수의 prototype의 prototype이 generator가 들어간다고 나옵니다. 즉 자기 마음대로 만든것이 아니라는 뜻입니다.

 

https://262.ecma-international.org/6.0/#sec-generatorfunction-objects

 

ECMAScript 2015 Language Specification – ECMA-262 6th Edition

5.1.1 Context-Free Grammars A context-free grammar consists of a number of productions. Each production has an abstract symbol called a nonterminal as its left-hand side, and a sequence of zero or more nonterminal and terminal symbols as its right-hand sid

262.ecma-international.org

 

next, return을 가지고 있는 prototype이 g1.prototype의 prototype인것을 볼 수 있습니다.

 

wrap

generator를 실제로 실행시킬 때 필요한 작업들을 하는 함수입니다. 계속 언급되던 '_invoke' 함수와 prototype을 이용한 상속이 진행됩니다.

 

이 작업이 끝나면 완전히 generator를 실행할 수 있습니다. 

function wrap(innerFn, outerFn, self, tryLocsList) {
      // If outerFn provided and outerFn.prototype is a Generator, then outerFn.prototype instanceof Generator.
      var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator;
      var generator = Object.create(protoGenerator.prototype); 
      var context = new Context(tryLocsList || []);
  
      // The ._invoke method unifies the implementations of the .next,
      // .throw, and .return methods.
      defineProperty(generator, "_invoke", { value: makeInvokeMethod(innerFn, self, context) });
      // 최종 리턴될때는 generator._invoke까지 있는 형태일것이다.
  
      return generator;
  }

 

매개변수 설명

 

  • innerFn
    • wrap 안에 들어가는 generator가 변환된 형태의 함수

  • outerFn
    • 내가 등록한 generator()
    • Generator 관련 prototype들을 mark() 호출 이후 세팅해둔 상태.
  • self
  • tryLocList
    • try, catch로 감싸져 있는 경우 배열로 try, catch block을 감싸서 관리한다. [ [1,3], [4,7] ]
    • 이 tryLocList는 switch-case 에서 try block을 실행 후 실패했을때 어떤 case로 넘어가야 하는지 도움을 줍니다. (generator에서는 감싸도 의미 X) 
    • async await을 변환했을때 try catch 와 함께 중요한 역할을 합니다.

 

기능

  • next(), return() 등을 호출했을때 generator를 처리해줄 함수를 호출하게 해줍니다.
    • mark 에서 next(), return() 등을 호출할 수 있게 되었지만, 실행시에 this._invoke is not a function 이라는 에러가 생겼습니다. 여기서 defineProperty를 통해 _invoke를 등록하면 정상 실행 됩니다.
  • var generator = Object.create(protoGenerator.prototype);
    • mark 에서 등록한 protoGenerator.prototype을 prototype으로 가지는 generator를 생성합니다.
    • 이 부분은 전체 과정이 이해되어야 알 수 있습니다. prototype이 어떻게 작동하는지 이해하고 스펙문서를 준수했구나. 라고 생각하면 편합니다.
  • defineProperty(generator, "_invoke", { value: makeInvokeMethod(innerFn, self, context) });
    • defineProperty에 의해 _invoke 함수가 생긴것을 볼 수 있습니다.
  • innerFn을 처리해주는 makeInvokeMethod 등록
  • generator를 실행하는데 필요한 함수, 값, 진행 상태등을 관리하는 Context 생성
    • generator를 실행할때 사용되는 실행 컨텍스트 역할

 

makeInvokeMethod

generator를 switch case의 형태로 변환합니다. 이 내용들을 실제로 실행하고 결과값을 반환하는 함수 입니다.

 

gen.next() 호출 -> this._invoke 호출 -> this._invoke에 등록된 makeInvokeMethod 실행 의 순서입니다.

next 안에 등록된 this._invoke _invoke는 makeInvokeMethod 호출

 

state를 이용해서 generator가 실행중이면 'executing', 실행 후에 generator가 끝나지 않으면 'yield', 끝나면 'complete' 상태로 변경됩니다. 브라우저는 상태를 internal slot을 통해서 관리하지만, 클로저를 이용해서 유사하게 만들었습니다. 

function makeInvokeMethod(innerFn, self, context) {
      var state = GenStateSuspendedStart;
  
      return function invoke(method, arg) {
        if (state === GenStateExecuting) {
          throw new Error("Generator is already running");
        }
  
        if (state === GenStateCompleted) {
          if (method === "throw") {
            throw arg;
          }
  
          // Be forgiving, per 25.3.3.3.3 of the spec:
          // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresume
          return doneResult();
        }
  
        context.method = method;
        context.arg = arg;
  
        while (true) {
          var delegate = context.delegate;
          if (delegate) {
            var delegateResult = maybeInvokeDelegate(delegate, context);
            if (delegateResult) {
              if (delegateResult === ContinueSentinel) continue;
              return delegateResult;
            }
          }
  
          if (context.method === "next") {
            // Setting context._sent for legacy support of Babel's
            // function.sent implementation.
            context.sent = context._sent = context.arg;
  
          } else if (context.method === "throw") {
            if (state === GenStateSuspendedStart) {
              state = GenStateCompleted;
              throw context.arg;
            }
  
            context.dispatchException(context.arg);
  
          } else if (context.method === "return") {
            context.abrupt("return", context.arg);
          }
  
          state = GenStateExecuting;
  
          var record = tryCatch(innerFn, self, context);
          if (record.type === "normal") {
            // If an exception is thrown from innerFn, we leave state ===
            // GenStateExecuting and loop back for another invocation.
            state = context.done
              ? GenStateCompleted
              : GenStateSuspendedYield;
  
            if (record.arg === ContinueSentinel) {
              continue;
            }
  
            return {
              value: record.arg,
              done: context.done
            };
  
          } else if (record.type === "throw") {
            state = GenStateCompleted;
            // Dispatch the exception by looping back around to the
            // context.dispatchException(context.arg) call above.
            context.method = "throw";
            context.arg = record.arg;
          }
        }
      };
    }

 

기능

  • var state = GenStateSuspendedStart
    • 위 state를 통해서 generator가 실행중인지, 끝났는지 계속 확인할 수 있습니다.
  • var record = tryCatch(innerFn, self, context)
    • innerFn.call(self,context) 를 통해서 switch-case가 실제로 호출 됩니다. call의 인자로 들어간 값을 통해서 innerFn에서 어떻게 Context를 받을 수 있었는지 알 수 있습니다.
function tryCatch(fn, obj, arg) {
	  try {
	    return { type: "normal", arg: fn.call(obj, arg) }; // self, context
	  } catch (err) {
	    return { type: "throw", arg: err };
	  }
}

function generator() {

// *******************************************************
// function generator$(_context)에 Context가 들어올 수 있는 이유
// tryCatch에서 call을 통해서 arg를 넘기는데 이때 arg는 Context 이다.
// *******************************************************

  return _regeneratorRuntime().wrap(function generator$(_context) {
    while (1)
      switch ((_context.prev = _context.next)) {
        case 0:
          _context.next = 2;
          return 1;
        case 2:
          return _context.abrupt("return", 4);
        case 3:
        case "end":
          return _context.stop();
      }
  }, _marked);
}
  • 함수를 실행하고 결과를 반환합니다.
    • IteratorResult 형태인 {value : xx, done : xx} 로 반환됩니다.
    • 따라서 generator.next().value 의 접근이 가능해지는 것입니다.

 

Context

마지막 내용입니다. generator가 브라우저 지원 함수와 유사하게 만들어지고, next 같은 함수도 등록했습니다.

하지만 이전에 어떤 값을 실행했고, 어떤 결과를 반환할지 이런 내용들은 어디서 관리할까요?

바로 Context에서 관리합니다. wrap 함수에서 확인했던 new Context() 를 확인하면 됩니다.

 

 function Context(tryLocsList) {
    // The root entry object (effectively a try statement without a catch
    // or a finally block) gives us a place to store values thrown from
    // locations where there is no enclosing try statement.
    this.tryEntries = [{ tryLoc: "root" }];
    tryLocsList.forEach(pushTryEntry, this);
    this.reset(true);
  }
  
  Context.prototype = {
    constructor: Context,

    reset: function(skipTempReset) {
      this.prev = 0;
      this.next = 0;
      // Resetting context._sent for legacy support of Babel's
      // function.sent implementation.
      this.sent = this._sent = undefined;
      this.done = false;
      this.delegate = null;

      this.method = "next";
      this.arg = undefined;

      this.tryEntries.forEach(resetTryEntry);

      if (!skipTempReset) {
        for (var name in this) {
          // Not sure about the optimal order of these conditions:
          if (name.charAt(0) === "t" &&
              hasOwn.call(this, name) &&
              !isNaN(+name.slice(1))) {
            this[name] = undefined;
          }
        }
      }
    },
    // 후략

여기가 prototype에 대해서 다시 공부하게 된 계기였습니다. 프로토타입 체인을 이용해  reset, stop, abrupt 같은 함수들에 접근하고 있었습니다. 

 

  • this.prev, this.next
    • switch-case에서 어떤 부분을 이어서 실행할지 기억합니다.
  • this.done
    • context 안에서 generator의 실행이 끝났는지 여부를 기억합니다.
    • 정상적으로 종료 되었다면 state를 'completed'로 변경하고 더이상 switch-case도 확인하지 않습니다.

  • this.method
    • 'return', 'next', 'throw' 처럼 어떤 method가 호출 되었는지 기록합니다.
  • this.arg
    • switch-case 실행 후 어떤 값을 반환했는지 this.arg에 넣습니다.

 

이 외에 여러 함수 stop, abrupt 같은 내용들도 정의되어 있습니다. 

이 부분은 스스로 확인 해보시면 좋을것 같습니다.

 

정리

generator의 polyfill이 어떻게 만들어지고, 어떻게 실행되는지 알아봤습니다.

 

처음에 봤던 코드를 다시 한번 확인 해보겠습니다.

var _marked = /*#__PURE__*/ _regeneratorRuntime().mark(generator);
function generator() {
  return _regeneratorRuntime().wrap(function generator$(_context) {
    while (1)
      switch ((_context.prev = _context.next)) {
        case 0:
          _context.next = 2;
          return 1;
        case 2:
          return _context.abrupt("return", 4);
        case 3:
        case "end":
          return _context.stop();
      }
  }, _marked);
}
var gen = generator();

gen.next().value; // 1
  1. _regeneratorRuntime 실행
    • generator polyfill prototype 생성
    • generator prototype에 next, throw, return 함수 등록
    • mark, wrap 함수 등록
    • 그 외..
  2. mark(generator) 실행
    • 내가 사용할 generator에 미리 브라우저와 유사하게 만든 generator prototype을 상속
    • _marked.next() 호출이 가능해지는 단계. 하지만 next 안에서 작동하는 함수 ('_invoke')가 등록되지 않아 에러 발생
  3. generator() 실행 이후 wrap()이 특정 iterator 반환.
    • 계속 언급되던 '_invoke' 함수 등록 및 prototype을 이용한 상속이 진행
    • Context 객체 등록 wrap의 innerFn generator$(_context) 에서 _context에 사용
  4. gen.next() 실행
    • gen의 prototype chain을 통해서 next() 실행 -> next() 안에서 this._invoke 호출
    • wrap 함수에서 연결한 makeInvokeMethod가 switch-case 실행 이후 {value, done} 형태의 IteratorResult 반환
    • 이후 gen.next().value로 값 접근

 

알게된 점 

  • minify 되어있는 코드는 원본 코드를 찾아가자. (암호문 푸는 시간이 아니다)
  • 실행 순서를 잘 파악하자.
    1. 함수가 시작하자마자 실행되는 내용
    2. 실제 초기화된 함수 호출 영역
  • 호출마다 다른 값을 반환하는 방법은 이렇게 switch case로 표현할 수 있구나.
    • if로 하면 if elseif 가 너무 많아서 가독성이 떨어질것 같다.
  • prototype chain을 이용해서 next, return 같은 함수들을 호출한다.
  • Context function은 prototype을 이용해서 기능 확장을 했다.
  • Object.create()를 상속으로 사용하는 실 예를 보았다.
    • Object.create(null) vs {} 의 차이도 명확하게 이해 되었다.
    • null로 초기화 할 경우 Object.prototype을 상속받지 않는다. (toString 같은것을 사용 못함)
  • internal slot은 직접 구현할 수 없으니 closure를 이용해서 값을 계속 가지고 있게 했다.
  • 스펙 문서와 거의 유사하게 구현이 되는구나.
  • 이런 함수가 왜 있어야 하지? 라고 생각이 들면 다 polyfill을 위한 내용들이다.

 

마치며

이번 글에서는 polyfill 함수가 어떻게 변환되고 실행되는지, 그리고 각 함수의 역할에 대해서 알아봤습니다.

 

하나의 파일을 읽는데 오랜 시간이 걸렸습니다.

오래 걸렸던 이유는 A를 이해하려면 B를 알아야하고 이런 형태였기 때문입니다.

  • Object.create 이해하기 -> prototype 공부 -> 다시 돌아와서 object.create 이해

 

그래도 지난번 react를 분석했을때 시간을 쏟았던 부분들은 이번에 좀 빠르게 넘어갈 수 있었습니다.

코드를 분석하기 위해 이해가 필요한 부분을 보충하면서 js를 이해 하는게 더 큰 것 같습니다.

 

async, await의 polyfill을 보면서 시작했는데, 이 이야기도 나중에 시간이 되면 4편으로 써보겠습니다.

 

감사합니다.

728x90
반응형