본문 바로가기

하늘기술

[ES6] 제너레이터

반응형

1. 제너레이터에 앞서

지난 포스팅(이터레이션)에서는 이터레이터이면서 이터러블인 객체에 대해서 살펴보았다. 이터레이터의 조건(next 메서드를 소유)과 이터러블의 조건([Symbol.iterator] 메서드 소유)을 만족 한다면 이는 이터레이터이면서 이터러블인 객체이다. 아래 코드에서 createInfinityByIteration 함수를 실행했을 때 반환되는 객체는 위의 조건을 모두 만족한다.

// 이터레이션 프로토콜을 구현하여 무한 이터러블을 생성하는 함수
const createIteration = function() {
  let i = 0; // 자유 변수
  return {
    [Symbol.iterator]() {
      return this;
    },
    next() {
      return { value: ++i };
    },
  };
};

for (const n of createInfinityByIteration())
  if (n > 5) break;
  console.log(n); // 1 2 3 4 5
}

for..of 문에서 이터레이터를 태우면 이터레이터가 가지고 있는 next메서드를 실행한다. next 메서드가 리턴한 객체는 다시 value와 done이라는 프러퍼티를 소유하는데 여기서 done이라는 프러퍼티가 for..of문을 계속 갈지 아니면 멈출지 결정하는 역할을 한다. 그리고 for..of문의 변수에는 next메서드가 리턴한 객체의 value값을 할당한다. 헉헉.. 이해하기 힘들겠지만 ECMA 형님들이 규칙으로 정한거라서 약간 외워야한다.

위의 코드에서는 next 메서드가 리턴할 객체에 done이라는 친구가 없으므로 이 이터레이터는 next를 호출할 때 마다 계속 해서 증가하는 무한 이터러블이라고 볼 수 있다. 위의 코드가 약간 지저분하지는 않지만 지저분한 뭔가 인터페이스로 지정했으면서도 복잡한 느낌을 지울 수가 없다. 그래서 ECMA 형님들이 제너레이터라는 이터러블을 생성하는 친구를 만들었다.

ES6에서 도입된 제너레이터(Generator) 함수는 이터러블을 생성하는 함수이다. 제너레이터 함수를 사용하면 이터레이션 프로토콜을 준수해 이터러블을 생성하는 방식보다 간편하게 이터러블을 구현할 수 있다. 위의 코드를 제너레이터로 구성하면 다음과 같다.

// 무한 이터러블을 생성하는 제너레이터 함수
function* createInfinityByGenerator() {
  let i = 0;
  while (true) {
    yield ++i;
  }
}

for (const n of createInfinityByGenerator()) {
  if (n > 5) break;
  console.log(n); // 1 2 3 4 5
}

image

제너레이터 함수는 일반 함수와 같이 함수의 코드 블록을 한 번에 실행하지 않는다. 함수 코드 블록의 실행을 일시 중지했다가 필요한 시점에 재시작할 수 있는 특수한 함수이다. 반복을 컨트롤 할 수 있는 친구라고 보면 된다. 아래의 코드를 보자 yield라고 선언한 친구가 반복을 어디까지할지 지정해준다.

function* counter() {
  console.log("첫번째 끝 ~~ 여기서 멈추세요");
  yield 1; // 첫번째 호출 시에 이 지점까지 실행된다.
  console.log("두번째 끝 ~~ 여기서 멈추세요");
  yield 2; // 두번째 호출 시에 이 지점까지 실행된다.
  console.log("세번째 끝 ~~ 여기서 멈추세요"); // 세번째 호출 시에 이 지점까지 실행된다.
}

const generatorObj = counter();

console.log(generatorObj.next()); // 첫번째 호출 {value: 1, done: false}
console.log(generatorObj.next()); // 두번째 호출 {value: 2, done: false}
console.log(generatorObj.next()); // 세번째 호출 {value: undefined, done: true}

일반 함수를 호출하면 return 문으로 반환 값을 리턴하지만 제너레이터 함수를 호출하면 제너레이터를 반환한다. 이 제너레이터는 이터러블(iterable)이면서 동시에 이터레이터(iterator)인 객체이다. 다시 말해 제너레이터 함수가 생성한 제너레이터는 Symbol.iterator 메소드를 소유한 이터러블이다. 그리고 제너레이터는 next 메소드를 소유하며 next 메소드를 호출하면 value, done 프로퍼티를 갖는 이터레이터 리절트 객체를 반환하는 이터레이터이다.

// 제너레이터 함수 정의
function* counter() {
  for (const v of [1, 2, 3]) yield v;
  // => yield* [1, 2, 3];
}

// 제너레이터 함수를 호출하면 제너레이터를 반환한다.
let generatorObj = counter();

// 제너레이터는 이터러블이다.
console.log(Symbol.iterator in generatorObj); // true

for (const i of generatorObj) {
  console.log(i); // 1 2 3
}

generatorObj = counter();

// 제너레이터는 이터레이터이다.
console.log("next" in generatorObj); // true

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

2. 제너레이터 함수의 정의

제너레이터 함수는 function* 키워드로 선언한다. 그리고 하나 이상의 yield 문을 포함한다.

// 제너레이터 함수 선언문
function* genDecFunc() {
    yield 1;
}

let generatorObj = genDecFunc();

// 제너레이터 함수 표현식
const genExpFunc = function* () {
    yield 1;
};

generatorObj = genExpFunc();

// 제너레이터 메소드
const obj = {
    generatorObjMethod() {
        yield 1;
    }
};

generatorObj = obj.generatorObjMethod();

// 제너레이터 클래스 메소드
class MyClass {

- generatorClsMethod() {
  yield 1;
  }
  }

const myClass = new MyClass();
generatorObj = myClass.generatorClsMethod();

3. 제너레이터 함수의 호출과 제너레이터 객체

제너레이터 함수를 호출하면 제너레이터 함수의 코드 블록이 실행되는 것이 아니라 제너레이터 객체를 반환한다. 앞에서 살펴본 바와 같이 제너레이터 객체는 이터러블이며 동시에 이터레이터이다. 따라서 next 메소드를 호출하기 위해 Symbol.iterator 메소드로 이터레이터를 별도 생성할 필요가 없다. 아래 예제를 살펴보자.

// 제너레이터 함수 정의
function* counter() {
  console.log("Point 1");
  yield 1; // 첫번째 next 메소드 호출 시 여기까지 실행된다.
  console.log("Point 2");
  yield 2; // 두번째 next 메소드 호출 시 여기까지 실행된다.
  console.log("Point 3");
  yield 3; // 세번째 next 메소드 호출 시 여기까지 실행된다.
  console.log("Point 4"); // 네번째 next 메소드 호출 시 여기까지 실행된다.
}

// 제너레이터 함수를 호출하면 제너레이터 객체를 반환한다.
// 제너레이터 객체는 이터러블이며 동시에 이터레이터이다.
// 따라서 Symbol.iterator 메소드로 이터레이터를 별도 생성할 필요가 없다
const generatorObj = counter();

// 첫번째 next 메소드 호출: 첫번째 yield 문까지 실행되고 일시 중단된다.
console.log(generatorObj.next());
// Point 1
// {value: 1, done: false}

// 두번째 next 메소드 호출: 두번째 yield 문까지 실행되고 일시 중단된다.
console.log(generatorObj.next());
// Point 2
// {value: 2, done: false}

// 세번째 next 메소드 호출: 세번째 yield 문까지 실행되고 일시 중단된다.
console.log(generatorObj.next());
// Point 3
// {value: 3, done: false}

// 네번째 next 메소드 호출: 제너레이터 함수 내의 모든 yield 문이 실행되면 done 프로퍼티 값은 true가 된다.
console.log(generatorObj.next());
// Point 4
// {value: undefined, done: true}

제너레이터 함수가 생성한 제너레이터 객체의 next 메소드를 호출하면 처음 만나는 yield 문까지 실행되고 일시 중단(suspend)된다. 또 다시 next 메소드를 호출하면 중단된 위치에서 다시 실행(resume)이 시작하여 다음 만나는 yield 문까지 실행되고 또 다시 일시 중단된다.

start -> generatorObj.next() -> yield 1 -> generatorObj.next() -> yield 2 -> ... -> end

next 메소드는 이터레이터 리절트 객체와 같이 value, done 프로퍼티를 갖는 객체를 반환한다. value 프로퍼티는 yield 문이 반환한 값이고 done 프로퍼티는 제너레이터 함수 내의 모든 yield 문이 실행되었는지를 나타내는 boolean 타입의 값이다. 마지막 yield 문까지 실행된 상태에서 next 메소드를 호출하면 done 프로퍼티 값은 true가 된다.

4. 제너레이터의 활용

4.1 이터러블의 구현

제너레이터 함수를 사용하면 이터레이션 프로토콜을 준수해 이터러블을 생성하는 방식보다 간편하게 이터러블을 구현할 수 있다. 이터레이션 프로토콜을 준수하여 무한 피보나치 수열을 생성하는 함수를 구현해 보자.

// 무한 이터러블을 생성하는 함수
const infiniteFibonacci = (function() {
  let [pre, cur] = [0, 1];

  return {
    [Symbol.iterator]() {
      return this;
    },
    next() {
      [pre, cur] = [cur, pre + cur];
      // done 프로퍼티를 생략한다.
      return { value: cur };
    },
  };
})();

// infiniteFibonacci는 무한 이터러블이다.
for (const num of infiniteFibonacci) {
  if (num > 10000) break;
  console.log(num); // 1 2 3 5 8...
}

이터레이션 프로토콜을 보다 간단하게 처리하기 위해 제너레이터를 활용할 수 있다. 제너레이터를 활용하여 무한 피보나치 수열을 구현한 이터러블을 만들어 보자.

// 무한 이터러블을 생성하는 제너레이터 함수
const infiniteFibonacci = (function*() {
  let [pre, cur] = [0, 1];

  while (true) {
    [pre, cur] = [cur, pre + cur];
    yield cur;
  }
})();

// infiniteFibonacci는 무한 이터러블이다.
for (const num of infiniteFibonacci) {
  if (num > 10000) break;
  console.log(num);
}

제너레이터 함수에 최대값을 인수를 전달해보자.

// 무한 이터러블을 생성하는 제너레이터 함수
const createInfiniteFibByGen = function*(max) {
  let [prev, curr] = [0, 1];

  while (true) {
    [prev, curr] = [curr, prev + curr];
    if (curr >= max) return; // 제너레이터 함수 종료
    yield curr;
  }
};

for (const num of createInfiniteFibByGen(10000)) {
  console.log(num);
}

이터레이터의 next 메소드와 다르게 제너레이터 객체의 next 메소드에는 인수를 전달할 수도 있다. 이를 통해 제너레이터 객체에 데이터를 전달할 수 있다.
스터디를 진행하면서 위의 코드가 정말 헷갈렸다. 위의 코드를 해석하기 전 기억해야할 규칙은 아래와 같다.

  • next 메소드는 이터레이터 리절트 객체와 같이 value, done 프로퍼티를 갖는 객체를 반환한다.
  • value 프로퍼티는 yield 문이 반환한 값이다.
  • 마지막 yield 문까지 실행된 상태에서 next 메소드를 호출하면 done 프로퍼티 값은 true가 된다.
  • next() 를 인자값과 함께 호출할 경우, 진행을 멈췄던 위치의 yield 문을 next() 메서드에서 받은 인자값으로 치환하고 그 위치에서 다시 실행하게 됩니다.
function* gen(n) {
  let res;
  res = yield n; // n: 0 ⟸ gen 함수에 전달한 인수

  console.log(res); // res: 1 ⟸ 두번째 next 호출 시 전달한 데이터
  res = yield res;

  console.log(res); // res: 2 ⟸ 세번째 next 호출 시 전달한 데이터
  res = yield res;

  console.log(res); // res: 3 ⟸ 네번째 next 호출 시 전달한 데이터
  return res;
}

const generatorObj = gen(0); //제너레이터 함수로 이터러블과 이터레이터의 성질을 가진 제너레이터 객체를 만든다. next가 호출되기 전까지 함수는 시작되지 않는다.

console.log(generatorObj.next()); //  ① 제너레이터 함수 시작, 결과 { value: 0, done: false }
console.log(generatorObj.next(1)); // ② 제너레이터 객체에 1 전달, 결과 { value: 1, done: false }
console.log(generatorObj.next(2)); // ③ 제너레이터 객체에 2 전달, 결과 { value: 2, done: false }
console.log(generatorObj.next(3)); // ④ 제너레이터 객체에 3 전달, 결과 { value: 3, done: true }

위의 코드를 나의 식대로 해석해보자.

  • generatorObj.next() 처음 실행과 함께 제너레이터 함수의 실행을 알린다. 처음 만나는 yield문 까지의 코드가 해석되고 실행된다. let res;까지 실행된다. yield 다음에 들어가있는 값이 제너레이터 객체의 value값이 된다. yield에는 n이라는 파라미터 변수의 값이 있음으로 현재 제너레이터의 value값은 0이다. 아직 뒤에 yield문이 계속해서 있고 return문을 안만났으니까 done에는 false 값이 찍힌다.
  • ② next()를 인자값과 함께 호출할 경우, 진행을 멈췄던 위치의 yield 문을 인자값으로 치환하고 그 위치에서 다시 실행한다. next의 인자로 1을 넣어주었음으로 해당 yeild n 구문은 1로 치환되고 res변수에는 1이 들어간다.
  • ③ - ②번 반복
  • ④ - ②번 반복 후 return문을 만나고 객체의 done프러퍼티는 true가 된다.

이터레이터의 next 메소드는 이터러블의 데이터를 꺼내 온다. 이에 반해 제너레이터의 next 메소드에 인수를 전달하면 제너레이터 객체에 데이터를 밀어 넣는다. 제너레이터의 이런 특성은 동시성 프로그래밍을 가능하게 한다.

4.2 비동기 처리

제너레이터를 사용해 비동기 처리를 동기 처리처럼 구현할 수 있다. 다시 말해 비동기 처리 함수가 처리 결과를 반환하도록 구현할 수 있다.

const fetch = require("node-fetch");

function getUser(genObj, username) {
  fetch(`https://api.github.com/users/${username}`)
    .then(res => res.json())
    // ① 제너레이터 객체에 비동기 처리 결과를 전달한다.
    .then(user => genObj.next(user.name));
}

// 제너레이터 객체 생성
const g = (function*() {
  let user;
  // ② 비동기 처리 함수가 결과를 반환한다.
  // 비동기 처리의 순서가 보장된다.
  user = yield getUser(g, "alalshow");
  console.log(user); // BeomKwan

  user = yield getUser(g, "ahejlsberg");
  console.log(user); // Anders Hejlsberg

  user = yield getUser(g, "ungmo2");
  console.log(user); // Ungmo Lee
})();

// 제너레이터 함수 시작
g.next();

① 비동기 처리가 완료되면 next 메소드를 통해 제너레이터 객체에 비동기 처리 결과를 전달한다.

② 제너레이터 객체에 전달된 비동기 처리 결과는 user 변수에 할당된다.

제너레이터을 통해 비동기 처리를 동기 처리처럼 구현할 수 있으나 코드는 장황해졌다. 따라서 좀 더 간편하게 비동기 처리를 구현할 수 있는 async/awit가 ES7에서 도입되었다.

위 예제를 async/awit 구현해 보자.

const fetch = require("node-fetch");

// Promise를 반환하는 함수 정의
function getUser(username) {
  return fetch(`https://api.github.com/users/${username}`)
    .then(res => res.json())
    .then(user => user.name);
}

async function getUserAll() {
  let user;
  user = await getUser("jeresig");
  console.log(user);

  user = await getUser("ahejlsberg");
  console.log(user);

  user = await getUser("ungmo2");
  console.log(user);
}

getUserAll();
반응형

'하늘기술' 카테고리의 다른 글

[ES6] 이터레이션  (1) 2020.03.27
[ES5] 불변성  (0) 2020.03.27
[ES6] Rest파라미터와 Spread연산자  (1) 2020.03.27
[ES5] 실행컨텍스트  (0) 2020.03.27
함수  (0) 2020.03.27