[모던자바스크립트] 제너레이터 정리

박재윤·2021년 1월 11일
0

자바스크립트

목록 보기
10/11

제너레이터는 하나의 값만을 반환하는 함수와 다르게 여러개의 값을 필요에 따라 하나씩 반환할 수 있다.

제너레이터 함수

제너레이터 함수를 만드려면 특별한 문법 구조, function*이 필요하다.

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

제너레이터는 일반 함수와 다르게 코드가 실행되지 않고 실행을 처리하는 특별한 객체가 반환된다. next()는 제너레이터의 주요 메서드로 next()를 호출하면 가장 가까운 yield <value>문을 만날 때까지 실행이 계속된다. 이후에 yield <value>문을 만나면 실행을 멈추고 값이 바깥 코드로 반환된다.

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

let generator = generateSequence();

let one = generator.next();

alert(JSON.stringify(one)); // {value: 1, done: false}

next()는 항상 value, done을 가진 객체를 반환한다.

제너레이터와 이터러블

제너레이터 객체는 iterable이다.

  • .next() 메서드가 있음
  • 반환 값의 형태는 {value: ..., done: true/false}이어야 함

next() 메서드를 보면서 짐작하셨듯이, 제너레이터는 이터러블 입니다.

제너레이터는 이터러블 객체이므로 for..of나 전개 문법도 사용할 수 있다.

Symbol.iterator 함수를 사용하면 제너레이터 함수로 반복이 가능하다.

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

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

alert( [...range] ); // 1, 2, 3, 4, 5

제너레이터 컴포지션

제너레이터 컴포지션은 제너레이터 안에 제너레이터를 임베딩 할 수 있게 해주는 제너레이터의 기능이다.

제너레이터의 특수 문법 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);

  // a..z
  yield* generateSequence(97, 122);

}

let str = '';

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

alert(str); // 0..9A..Za..z

yield*를 사용하면 사용한 제너레이터를 대상으로 반복으로 수행하고 산출값들을 밖으로 전달하는 것이다. 제너레이터 컴포지션을 사용하면 제너레이터의 흐름을 다른 제너레이터에 삽입할 수 있다.

yield를 사용해 제너레이터 안 밖 정보 교환

yield는 결과를 바깥으로 전달할 뿐 아니라 제너레이터 안으로 전달하기도 한다.

function* gen() {
  // 질문을 제너레이터 밖 코드에 던지고 답을 기다립니다.
  let result = yield "2 + 2 = ?"; // (*)

  alert(result);
}

let generator = gen();

let question = generator.next().value; // <-- yield는 value를 반환합니다.

generator.next(4); // --> 결과를 제너레이터 안으로 전달합니다.

제너레이터 안으로 다음과 같이 에러를 전달할 수도 있다.

function* gen() {
  try {
    let result = yield "2 + 2 = ?"; // (1)

    alert("위에서 에러가 던져졌기 때문에 실행 흐름은 여기까지 다다르지 못합니다.");
  } catch(e) {
    alert(e); // 에러 출력
  }
}

let generator = gen();

let question = generator.next().value;

generator.throw(new Error("데이터베이스에서 답을 찾지 못했습니다.")); // (2)

Async 이터레이터와 제너레이터

async iterator를 사용하면 비동기적으로 들어오는 데이터를 필요에 따라 처리할 수 있다.

이터러블 객체를 비동기적으로 만드려면

  • Symbol.iterator 대신 Symbol.asyncIterator를 사용해야한다.
  • next()는 프로미스를 반환해야한다.
  • 비동기 이터러블 객체를 대상으로 하는 반복 작업은 for await (let item of iterable) 반복문을 사용해 처리해야한다.
let range = {
  from: 1,
  to: 5,

  // for await..of 최초 실행 시, Symbol.asyncIterator가 호출됩니다.
  [Symbol.asyncIterator]() { // (1)
    // Symbol.asyncIterator 메서드는 이터레이터 객체를 반환합니다.
    // 이후 for await..of는 반환된 이터레이터 객체만을 대상으로 동작하는데,
    // 다음 값은 next()에서 정해집니다.
    return {
      current: this.from,
      last: this.to,

      // for await..of 반복문에 의해 각 이터레이션마다 next()가 호출됩니다.
      async next() { // (2)
        //  next()는 객체 형태의 값, {done:.., value :...}를 반환합니다.
        // (객체는 async에 의해 자동으로 프라미스로 감싸집니다.)

        // 비동기로 무언가를 하기 위해 await를 사용할 수 있습니다.
        await new Promise(resolve => setTimeout(resolve, 1000)); // (3)

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

(async () => {

  for await (let value of range) { // (4)
    alert(value); // 1,2,3,4,5
  }

})()

async 제너레이터

일반 제너레이터에서는 await를 사용할 수 없다. 그런데 제너레이터의 본문에서 await를 사용해야하는 상황이 발생하면 제너레이터 앞에 async를 붙여주면 된다.

그러면 for await...of로 반복이 가능한 async 제너레이터를 사용할 수 있게 된다.

async 제너레이터의 next() 메서드는 비동기적으로 프로미스를 반환한다.

0개의 댓글