제너레이터와 async/await

윤석주·2022년 9월 12일
0

javascript

목록 보기
13/13

ES6에서 도입된 제너레이터는 코드 블록의 실행을 일시 중지했다가 필요한 시점에 재개할 수 있는 특수한 함수입니다. 일반 함수와 구별되는 다음과 같은 특징을 갖고 있습니다.

  • 제너레이터 함수는 함수 호출자에게 함수 실행의 제어권을 양도할 수 있다.
    • 일반 함수를 호출하면 제어권이 함수로 넘어가고 일괄 실행된다.
    • 제너레이터 함수는 함수 실행을 함수 호출자가 제어할 수 있다.
    • 이는 함수의 제어권을 함수가 독점하는 것이 아니라 함수 호출자에게 양도(yield) 할 수 있다는 것을 의미한다.
  • 제너레이터 함수는 함수 호출자와 함수의 상태를 주고받을 수 있다.
    • 일반 함수는 함수가 실행되고 있는 동안 함수 외부에서 내부로 값을 전달하여 상태를 변경할 수 없다.
    • 제너레이터 함수는 함수 호출자와 양방향으로 함수의 상태를 주고 받을 수 있다.
  • 제너레이터 함수를 호출하면 제너레이터 객체를 반환한다.
    • 제너레이터 함수를 호출하면 함수 코드를 실행하는 것이 아니라 이터러블이면서 이터레이터인 제너레이터 객체를 반환한다.

제너레이터 함수의 정의

제너레이터 함수는 function* 키워드로 선언합니다. 그리고 하나 이상의 yield 표현식을 포함합니다. 이것을 제외하곤 일반 함수를 정의하는 방법과 같습니다.

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

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

// 제너레이터 메서드
const obj = {
  * genObjMethod() {
    yield 1;
  }
};

// 제너레이터 클래스 메서드
class MyClass {
  * getClsMethod() {
    yield 1;
  }
}

애스터리스크(*)의 위치는 function 키워드와 함수 이름 사이라면 어디든 상관 없지만 일관성 유지를 위해 function 키워드 바로 뒤에 붙이는 것을 권장합니다.

제너레이터 함수는 화살표 함수로 정의할 수 없으며, new 연산자와 함께 생성자 함수로 호출할 수 없습니다.

제너레이터 객체

제너레이터 함수를 호출하면 일반 함수처럼 함수 코드 블록을 실행하는 것이 아니라 제너레이터 객체를 생성해 반환한다고 했습니다. 또한 반환된 제너레이터 객체는 이터러블이면서 동시에 이터레이터 입니다.

제너레이터 객체는 next 메서드를 갖고 있는 이터레이터이며 이터레이터에는 없는 return, throw 메서드를 갖습니다. 제너레이터 객체의 세 개의 메서드는 호출시 다음과 같이 동작합니다.

  • next 메서드를 호출하면 제너레이터 함수의 yield 표현식까지 코드 블록을 실행하고 yield된 값을 value 프로퍼티 값으로, false를 done 프로퍼티 값으로 갖는 이터레이터 리절트 객체를 반환한다.

  • return 메서드를 호출하면 인수로 전달받은 값을 value 프로퍼티 값으로, true를 done 프로퍼티 값으로 갖는 이터레이터 리절트 객체를 반환한다.

  • function* genFunc() {
      try{
        yield 1;
        yield 2;
        yield 3;
      } catch (e) {
        console.error(e);
      }
    }
    
    const generator = genFunc();
    
    console.log(generator.next()); // {value: 1, done: false}
    console.log(generator.return('End!')); // {value: "End!", done: true}
  • throw 메서드를 호출하면 인수로 전달받은 에러를 발생시키고 undefined를 value 프로퍼티로, true를 done 프로퍼티 값으로 갖는 이터레이터 리절트 객체를 반환한다.

  • const generator = genFunc();
    
    console.log(generator.next()); // {value: 1, done: false}
    console.log(generator.throw('Error!')); // {value: undefined, done: true}

제너레이터의 일시 중지와 재개

제너레이터는 yield 키워드와 next 메서드를 통해 실행을 일시 중지했다가 필요한 시점에 다시 재개할 수 있습니다. 제너레이터의 next 메서드를 호출하면 제너레이터 함수의 코드 블록을 실행하지만 일반 함수처럼 한 번에 코드 블록의 모든 코드를 일괄 실행하는 것이 아닌, yield 표현식 까지만 실행합니다. yield 키워드는 제너레이터 함수의 실행을 일시 중지시키거나 yield 키워드 뒤에 오는 표현식의 평가 결과를 제너레이터 함수 호출자에게 반환합니다.

next 메서드를 반복 호출하여 yield 표현식까지 실행과 일시 중지를 반복하다가 제너레이터 함수가 끝까지 실행되면 next 메서드가 반환하는 이터레이터 리절트 객체의 value 프로퍼티에는 제너레이터 함수의 반환값이 할당되고(명시적 return이 없다면 undefined) done 프로퍼티에는 true가 할당됩니다.

generator.next() -> yield -> generator.next() -> yield -> ... -> generator.next() -> return

제너레이터 객체의 next 메서드에 전달한 인수는 제너레이터 함수의 yield 표현식을 할당받는 변수에 할당됩니다.

function* genFunc() {
  const x = yield 1;
  
  const y = yield (x + 10);
  
  return x+y;
}

const generator = genFunc(0);

let res = generator.next();
console.log(res); // {value:1, done: false}

res = generator.next(10);
console.log(res); // {value: 20, done: false}

res = generator.next(20);
console.log(res); // {value: 30, done: true}

이처럼 제너레이터 함수는 next 메서드와 yield 표현식을 통해 함수 호출자와 함수의 상태를 주고 받을 수 있습니다.

제너레이터의 활용

이터러블의 구현

무한 피보나치를 구현해보겠습니다.

const infiniteFibonacci = (function* () {
  let [pre, cur] = [0, 1];
  
  while (true) {
    [pre, cur] = [cur, pre + cur];
    yield cur;
  }
}());

for (const num of infiniteFibonacci) {
  if (num > 10000) break;
  console.log(num); // 1 2 3 5 8 ... 2584 4181 6765
}

비동기 처리

제너레이터 함수의 특성을 이용해 프로미스의 후속 처리 메서드 then/catch/finally 없이 비동기 처리 결과를 반환하도록 구현할 수 있습니다.

// 제너레이터 실행기
const async = generatorFunc => {
  const generator = generatorFunc();
  
  const onResolved = arg => {
    const result = generator.next(arg);
    
    return result.done
      ? result.value
      : result.then(res => onResolved(res));
  };
  
  return onResolved;
};

(async(function* fetchTodo() {
  const url = 'https://jsonplaceholder.typicode.com/todos/1';
  
  const response = yield fetch(url);
  const todo = yield response.json();
  console.log(todo);
  // {userId: 1, id: 1, title: 'delectus aut autem', completed: false}
})());

위 예제의 제너레이터 함수를 실행하는 제너레이터 실행기인 async 함수는 이해를 돕기 위해 간략화한 예제이므로 완전하지 않습니다. async/await을 사용하면 async 함수와 같은 제너레이터 실행기를 사용할 필요가 없지만 혹시 제너레이터 실행기가 필요하다면 직접 구현하는 것보다 co 라이브러리를 사용하는 것이 추천됩니다.

const co = require('co');

co(function* fetchTodo() {
  const url = 'https://jsonplaceholder.typicode.com/todos/1';
  
  const response = yield fetch(url);
  const todo = yield response.json();
  console.log(todo);
  // {userId: 1, id: 1, title: 'delectus aut autem', completed: false}
});

출처

  • 모던 자바스크립트 Deep Dive, 이웅모, 위키북스
profile
웹 개발을 공부하고 있는 윤석주입니다.

0개의 댓글