Generator

GeneratorES6에서 도입되었으며 Iterable을 생성하는 함수다. Generator를 사용하면 Iteration Protocol을 사용하여 Iterable을 생성하는 방식보다 간편하다.
Iteration Protocol에 대한 자세한 내용은 다음을 참고하길 바란다.

Generator 함수의 정의

Generator 함수는 코드 블록을 한 번에 실행하지 않고 함수 코드 블록의 실행을 중지했다가 필요한 시점에 다시 시작할 수 있는 함수다.
Generator 함수는 function * 키워드를 사용하며 하나 이상의 yield 문을 포함한다.

// 함수 선언문
function* decFunc() {
  yield 1;
}

let genObj = decFunc();

// 함수 표현식
const expFunc = function*() {
  yield 1;
};

genObj = expFunc();

// 메서드
const obj = {
  *objectMethod() {
    yield 1;
  }
};

genObj = obj.objectMethod();

// 클래스 메서드
class GenClass {
  *classMethod() {
    yield 1;
  }
}

const genClass = new GenClass();
genObj = genClass.classMethod();

Generator 객체

Generator 함수를 호출하면 코드 블록이 실행되는 것이 아니라 Generator 객체를 반환한다. Generator 객체는 이터러블이면서 동시에 이터레이터다. 따라서 Symbol.iterator를 사용하여 이터레이터를 생성할 필요 없다.

function* counter() {
  console.log("First");
  yield 1;
  console.log("Second");
  yield 2;
  console.log("Third");
  yield 3;
  console.log("The end");
}

const genObj = counter();

console.log(genObj.next()); //{value: 1, done: false}
console.log(genObj.next()); //{value: 2, done: false}
console.log(genObj.next()); //{value: 3, done: false}
console.log(genObj.next()); //{value: undefined, done: true}

Generator 객체는 이터러블이면서 이터레이터이기 때문에 next()메서드를 가지고 있다. 따라서 next() 메서드를 호출하면 yield문까지 실행되고 일시 중지된다. 다시 next() 메서드를 호출하면 다음 yield문을 만날 때까지 실행된 뒤 일시 중지된다.

Generator 객체를 이용한 이터러블 구현

const genObj = (function*() {
  let i = 0;

  while (true) {
    yield ++i;
  }
})();

for (let item of genObj) {
  if (item === 10) break;
  console.log(item);
}
// Generator 함수에 파라미터 전달
const genObj = function*(max) {
  let i = 0;

  while (true) {
    if (i === max) break;
    yield ++i;
  }
};

for (let item of genObj(10)) {
  console.log(item);
}
// next 메서드에 파라미터 전달
function* genFunc(n) {
  let res;
  res = yield n;

  console.log(res);
  res = yield res;

  console.log(res);
  res = yield res;

  console.log(res);
  return res;
}
const genObj = genFunc(0);

console.log(genObj.next());
console.log(genObj.next(1));
console.log(genObj.next(2));
console.log(genObj.next(3));

Generator를 이용한 비동기 처리

Generator의 진면목은 비동기 프로그래밍에서 볼 수 있다. 함수가 실행 도중에 멈춘다니. 언제 응답이 올지 알 수 없기 때문에, callback을 등록하는 비동기 프로그래밍에 응용하면 callback hell을 탈출할 수 있지 않을까?

function getId(phoneNumber) {
  // …
  iterator.next(result);
}

function getEmail(id) {
  // …
  iterator.next(result);
}

function getName(email) {
  // …
  iterator.next(result);
}

function order(name, menu) {
  // …
  iterator.next(result);
}
function* orderCoffee(phoneNumber) {
  const id = yield getId(phoneNumber);
  const email = yield getEmail(id);
  const name = yield getName(email);
  const result = yield order(name, "coffee");
  return result;
}

const iterator = orderCoffee("010-1234-1234");
iterator.next();

Generator는 어떻게 구현되어 있을까?

// ES6
function* foo() {
  yield bar();
}

// ES5 Compiled
("use strict");

var _marked = /*#__PURE__*/ regeneratorRuntime.mark(foo);

function foo() {
  return regeneratorRuntime.wrap(
    function foo$(_context) {
      while (1) {
        switch ((_context.prev = _context.next)) {
          case 0:
            _context.next = 2;
            return bar();
          case 2:
          case "end":
            return _context.stop();
        }
      }
    },
    _marked,
    this
  );
}

Genrator는 결국 iterable Protocol를 구현하는 객체이다. 그러나 프로토콜과 관련된 어느것도 보이지 않는다.

대신 regeneratorRuntime이 보인다.

babel에서는 regeneratorRuntime라이브러리를 사용해서 구현을 했다.

코드의 역사를 따라가다 보면 facebook/regenerator repository에 도달하게 된다.

이 라이브러리는 2013년 Node.js v0.11.2에서 generator syntax를 지원하기 위해 만들어 졌으며, Babel에서는 이 라이브러리를 사용하여 generator를 구현하고 있다. 실제 코드를 들여다보면 Symbol과 Iterator를 이용해서 Iterable Protocol을 구현하고 있다.


🙏 Reference