본문 바로가기

Language/JavaScript

<JS> 제너레이터

1. 제너레이터

* 제너레이터란

 - 일반적으로 함수는 0개 또는 하나의 값을 반환한다. 하지만 제너레이터(generator)는 반환값을 여러개로 만들 수 있다.

 

 - 제너레이터는 일반적인 함수가 아닌 제너레이터 함수를 통해 만들게 된다.

 

* 제너레이터 만들기

 - 제너레이터는 '*' 키워드와 함께 만들 수 있다.

 

function* test() {
  yield 1;
  yield 2;
  return 3;
}

const generator = test();

const one = generator.next();
const two = generator.next();
const three = generator.next();

console.log(one); // { value: 1, done: false }
console.log(two); // { value: 2, done: false }
console.log(three); // { value: 3, done: true }

 

 - 위 코드에서 보이는 일반함수와 가장 큰 차이점은 함수 호출시 코드가 실행되지 않는다는 점이다. 제너레이터 함수는 호출되면 제너레이터 객체가 반환된다.

 

 - next는 제너레이터의 핵심 메서드로, 호출될 때 yield <value> 문을 만날 때 까지 실행이 지속된다. yield 문을 만나면 실행이 멈추고 value가 반환된다. 이 때 next는 value 뿐아니라 함수 코드 실행의 종료를 알리는 done 프로퍼티도 함께 담아서 객체를 반환한다.

 

2. 이터러블과 제너레이터

* 이터레이터

 - 반복문은 이터레이터(iterator, next 가 있는 객체)를 대상으로 동작하며, 다음 값이 필요할 때마다 value와 done 프로퍼티를 가진 객체를 반환하는 next() 메서드를 호출한다. 따라서 next 가 존재하는 제너레이터는 이터러블(반복가능한)객체이다. 

 

const range = {
  from: 1,
  to: 10
};

 

- 제너레이터가 이터러블임을 확실히 하기위해 이터레이터를 통해 이터러블 객체를 만들어보자. 위의 객체를 반복가능하도록 만드는 코드는 아래와 같다.

 

range[Symbol.iterator] = function() {

  return {
    current: this.from,
    last: this.to,

    next() {
      if (this.current <= this.last) {
        return { done: false, value: this.current++ };
      } else {
        return { done: true };
      }
    }
  };
};

 

 - 반복문(for...of)가 호출될 때, Symbol.iterator가 호출된다. 이는 이터레이터 객체를 반환하게되고, 이후부터 for...of는 반환된 이터레이터 객체만을 대상으로 동작한다.

 

 - 이제 반복마다 next() 가 호출되는데, 이는 값을 반드시 done과 value가 갖춰진 객체로 만들어 반환해야한다. 그래서 위의 코드는 조건을 확인하여 이 객체를 반환하도록 했다.

 

 - 범위가 to 에 도달했을 때, done true를 통해 끝나도록 하였다.

 

for (let num of range) {
  console.log(num); // 1, 2, 3, ... , 10
}

 

 - 반복문에서 원하는 대로 값을 얻는 것을 확인할 수 있다.

 

 - 이터레이터 객체와 반복 대상 객체를 합쳐서 range 자체를 이터레이터로 만들면 코드가 더 간결해진다.

 

const range = {
  from: 1,
  to: 10,

  [Symbol.iterator]() {
    this.current = this.from;
    return this;
  },

  next() {
    if (this.current <= this.to) {
      return { done: false, value: this.current++ };
    } else {
      return { done: true };
    }
  }
};

 

 - 코드를 보면 알겠지만 제너레이터와 비슷한 점이 많다. next 메서드가 있으며 반환되는 객체가 done과 value 프로퍼티를 갖고있다. 즉 제너레이터는 이터러블이다.

 

* 제너레이터

 - 제너레이터가 내부적으로 next가 있음을 알고 있고, 위에서 next가 있으면 이터러블 이라고 했으니 반복문을 통해 알아보자.

 

function* test() {
  yield 1;
  yield 2;
  return 3;
}

const generator = test();
const generator2 = test();

for (let value of generator) {
  console.log(value); // 1, 2
}
console.log([...generator2]) // [1, 2]

 

 - next().value를 호출하는 것보다 더 깔끔하게 value 만을 출력하게 하였다. 보다시피 반복문이 잘 돌고 있다. 그러나 한가지 이상한 점이 있다. 바로 3이 출력되지 않은것이다.

 

 - 위에 이터레이터 예제에서 봤듯이 done:true일 경우는 반복문에서 무시된다. 3을 출력하려면 yield 3; 을 추가해야한다.

 

 - 이터레이터 예제에서 봤던 range를 제너레이터 함수로 구현하면 다음과 같다.

 

let range = {
  from: 1,
  to: 5,

  *[Symbol.iterator]() { // [Symbol.iterator]: function*()를 짧게 줄임
    for(let value = this.from; value <= this.to; value++) {
      yield value;
    }
  }
};

 

 - 반환 값이 자동으로 {value: ??, done: ??} 형태이므로 코드가 훨씬 짧아졌음을 볼 수 있다.

 

3. 제너레이터의 기능

* 제너레이터 컴포지션

 - 제너레이터 컴포지션(generator composition)을 제너레이터 안에 제너레이터를 임베딩하는 기능이다. 

 

 - 숫자 0부터 9까지 (문자코드 48~57), 알파벳 대문자 A부터 Z까지(문자 코드 65~90)을 연속으로 가지는 비밀번호를 만든다고 가정하자. 위 두가지 로직이 별개의 제너레이터라고 가정하자. 그렇다면 여러개의 함수를 만들고, 그 결과를 조합해야한다. 하지만 제너레이터의 yield* 키워드를 사용하면 제너레이터를 다른 제너레이터에 넣을 수 있다.

 

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) yield i;
}

function* generatePasswordCodes() {
  // 0..9
  yield* generateSequence(48, 57);
  // A..Z
  yield* generateSequence(65, 90);
}

let str = '';

for (let code of generatePasswordCodes()) {
  str += String.fromCharCode(code);
}

console.log(str); // 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ

 

 - yield* 는 실행자체를 다른 제너레이터에 위임하는 기능을 갖고 있다. 이를 통해 마치 바깥의 제너레이터가 값을 생성한것처럼 동작하는 것이다.

 

* 양방향 교환

 - 지금까지의 제너레이터는 바깥에 값을 전달하기만 했다. 그러나 제너레이터는 값을 안으로 전달하기도 한다.

 

 - 값을 안으로 전달할때에는 next(arg)를 호출한다. 인수인 arg는 yield의 결과 그 자체가 된다.

 

function* gen() {
  const q1 = yield '2 + 2 = ?';
  console.log(q1);
  const q2 = yield '3 + 3 = ?';
  console.log(q2);
}

const generator = gen();

console.log(generator.next().value);
console.log(generator.next(4).value);
console.log(generator.next(9).done);

// 2 + 2 = ?
// 4
// 3 + 3 = ?
// 9
// true

 

 - next를 처음 호출할 땐 인수가 필요없다. 첫 yield의 결과가 반환되어 question에 담긴다. 이 때 제너레이터는 yield 줄에 멈춘다.

 

 - 다음으로 next(4)를 호출하였다. 이 때, q1에 4가 담기게된다. 그리고 다음 yield까지 실행되므로 console.log(q1)이 실행되고 두 번째 yield 의 결과가 반환된 후 멈춘다.

 

 - 위 과정이 반복되어 마지막에는 yield가 더 이상 없으므로 done 이 true로 반환된다.

 

 - 모던 자바스크립트에서는 제너레이터를 잘 사용하지 않지만 제너레이터 호출 코드와 데이터를 교환할 수 있기 때문에 유용하고, 이터러블 객체를 쉽게 만들 수 있다는 장점이 있다. 그리고 비동기 제너레이터를 활용하여 데이터 스트림을 다룰 때에도 유용하다.

 

 


 

참고

 

 

 

 

제너레이터

 

ko.javascript.info

 

 

 

iterable 객체

 

ko.javascript.info

 

'Language > JavaScript' 카테고리의 다른 글

<ES2021> ES2021 Features  (0) 2021.10.25
<ES2020> ES2020 Features  (0) 2021.10.24
<ES10> ES2019 Features  (0) 2021.10.11
<ES9> ES2018 Features  (0) 2021.10.10
<이벤트루프> 태스크 큐 : 마이크로태스크 큐 & 매크로태스크 큐  (0) 2021.10.04