thumbnail
generator
JS
2022.08.26

코어 자바스크립트의 제너레이터 부분을 읽고 정리한 내용입니다.

제너레이터

일반 함수는 하나의 값(혹은 0개의 값)만을 반환하지만 제너레이터를 사용하면 여러 개의 값을 필요에 따라 하나씩 반환(yield)할 수 있다. 제너레이터와 이터러블 객체를 함께 사용하면 손쉽게 데이터스트림을 만들 수 있다.

제너레이터 함수

제너레이터를 만들려면 ‘제너레이터 함수’라 불리는 특별한 문법 구조, function* 이 필요하다

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

제너레이터 함수는 일반 함수와 동작 방식이 다르다. 제너레이터 함수를 호출하면 코드가 실행되지 않고, 대신 실행을 처리하는 특별 객체, ‘제너레이터 객체’가 반환된다.

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}
// 제너레이터 함수는 제너레이터 객체를 생성한다.
let generator = generateSequence();
console.log(generator); // [object Generator]

함수 본문 코드는 아직 실행되지 않았다. next() 는 제너레이터의 주요 메서드이다. next()를 호출하면 가장 가까운 yield 문을 만날 때까지 실행이 지속된다.(value를 생략할 수도 있는데 undefined가 된다.) 이후, yield 문을 만나면 실행이 멈추고 산출하고자 하는 값인 value가 바깥 코드에 반환된다. (yield 는 ‘생산하다, 산출하다’ 라는 뜻을 가진다.). next()는 항상 아래 두 프로퍼티를 가진 객체를 반환한다.

  • value: 산출 값
  • done: 함수 코드 실행이 끝났으면 true, 아니면 false

제너레이터를 만들고 첫 번째 산출 값을 받는 예시

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}
let generator = generateSequence();
let one = generator.next();
alert(JSON.stringify(one)); // {"value":1,"done":false}

현재 첫 번째 값만 받았으므로 함수 실행은 두 번째 줄에서 멈췄다. generator.next()를 다시 호출하면 실행이 재개되고 다음 yield를 반환한다.

let two = generator.next();
alert(JSON.stringify(two)); // {"value": 2, "done": false}

generator.next()를 다시 호출하면 실행은 return 문에 다다르고 함수가 종료된다.

let three = generator.next();
alert(JSON.stringify(three)); // {"value": 3, "done": true}

제너레이터와 이터러블

next() 메서드를 보면서 짐작하셨듯이, 제너레이터는 이터러블이다. 따라서 for…of 반복문을 통해 값을 얻을 수 있다.

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}
let generator = generateSequence();
for(let value of generator) {
  alert(value); // 1, 2
}

주의점: for…of 이터레이션이 done:true 일 때 마지막 value 를 무시하기 때문에 모든 값이 출력되기를 원한다면 yield로 값을 반환해야 한다.

제너레이터는 이터러블 객체이므로 전개 문법같은 관련 기능을 사용할 수 있다.

이터러블 대신 제너레이터 사용하기

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

rangeSymbol.iterator 는 제너레이터를 반환하고, 제너레이터 메서드는 for…of 가 동작하는데 필요한 사항을 총족시키므로 예시가 잘 동작한다.

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

제너레이터 컴포지션

제너레이터 컴포지션은 제너레이터 안에 제너레이터를 임베딩할 수 있게 해주는 제너레이터의 특별 기능이다. 먼저, 연속된 숫자를 생성하는 제너레이터 함수를 만들어보자.

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

그리고 이 함수를 기반으로 좀 더 복잡한 값을 연속해서 생성하는 함수를 만들어 보자.

  • 처음엔 숫자 0 부터 9 까지를 생성한다. (문자 코드 48 부터 57까지)
  • 이어서 알파벳 대문자 A 부터 Z 까지 생성한다. (문자 코드 65부터 90까지)
  • 이어서 알파벳 소문자 a 부터 z 까지 생성한다. (문자 코드 97부터 122까지)

제너레이터 특수 문법 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* gen 이 제너레이터 gen을 대상으로 반복을 수행하고, 산출 값들을 바깥으로 전달한다는 것을 의미한다.

‘yield’를 사용해 제너레이터 안,밖으로 정보 교환하기

제너레이터는 값을 생성해주는 특수 문법을 가진 이터러블 객체와 유사하다 그런데 제너레이터는 더 강력하고 유연한 기능을 제공한다. yield가 양방향 길과 같은 역할을 하기 때문이다. yield는 결과를 바깥으로 전달할 뿐만 아니라 값을 제너레이터 안으로 전달하기까지 한다. 값을 안,밖으로 전달하려면 generator.next(arg)를 호출해야 한다. 이때 인수 arg는 yield의 결과가 된다.

function* gen() {
  // 질문을 제너레이터 밖 코드에 던지고 답을 기다린다.
  let result = yield "2 + 2 = ?"; // (*)
  alert(result);
}
let generator = gen();
let question = generator.next().value; // <-- yield는 value를 반환.
generator.next(4); // 결과를 제너레이터 안으로 전달.
  1. generator.next()를 처음 호출할 땐 항상 인수가 없어야 한다. 인수가 넘어오더라도 무시되어야 한다. generator.next()를 호출하면 실행이 시작되고 첫 번째 yield “2+2=?”의 결과가 반환된다. 이 시점에는 제너레이터가 (*)로 표시한 줄에서 실행을 잠시 멈춘다.
  2. 그 후, yield의 결과가 제너레이터를 호출하는 외부 코드에 있는 변수, question에 할당된다.
  3. 마지막 줄, generator.next(4)에서 제너레이터가 다시 시작되고 4 는 result에 할당 된다. (let result = 4).

외부 코드에선 next(4)를 즉시 호출하지 않고 있다는 것에 주목하자 제너레이터가 기다려주기 때문에 호출을 나중에 해도 문제가 되지 않는다.

// 일정 시간이 지난 후 제너레이터가 다시 시작됨
setTimeout(() => generator.next(4), 1000);

일반 함수와 다르게 제너레이터 외부 호출 코드는 next/yield를 이용해 결과를 전달 및 교환한다.

function* gen() {
  let ask1 = yield "2 + 2 = ?";
  alert(ask1); // 4
  let ask2 = yield "3 * 3 = ?";
  alert(ask2); // 9
}
let generator = gen();
alert(generator.next().value); // "2 + 2 = ?"
alert(generator.next(4).value); // "3 * 3 = ?"
alert(generator.next(9).value); // true
  1. 제너레이터 객체가 만들어지고 첫번째 .next()가 호출되면 실행이 시작되고 첫 번째 yield에 다다른다.
  2. 산출 값은 바깥 코드로 반환된다.
  3. 두 번째 .next(4)는 첫 번째 yield의 결과가 될 4를 제너레이터 안으로 전달한다. 그리고 다시 실행이 이어진다.
  4. 실행 흐름이 두 번째 yield에 다다르고, 산출 값(“3 * 3 = ?“)이 제너레이터 호출 결과가 됩니다.
  5. 세 번째 next(9)는 두 번째 yield의 결과가 될 9를 제너레이터 안으로 전달한다. 그리고 다시 실행이 이어지는데, done: true이므로 제너레이터 함수는 종료된다.

첫 번째 next()를 제외한 각 next(value)는 현재 yield의 결과가 될 값을 제너레이터 안에 전달한다. 그리고 다음 yield의 결과로 되돌아온다.

generator.throw

에러를 yield 안으로 전달하려면 generator.throw(err)를 호출해야 한다. generator.throw(err) 를 호출하게 되면 err 는 yield가 있는 줄로 던져진다.

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)

(2)에서 제너레이터 안으로 던진 에러는 yield와 함께 라인 (1)에서 예외를 만든다. 예외는 try…catch 문에서 잡히고, 관련 정보가 alert창에 출력된다. 제너레이터 안에서 예외를 처리하지 않았다면 예외는 여타 예외와 마찬가지고 제너레이터 호출 코드(외부코드)로 떨어져 나온다. generator.throw(라인 (2))에서 제너레이터를 호출하고 있으므로 아래와 같이 에러를 여기서 잡아도 된다.

function* generate() {
  let result = yield "2 + 2 = ?"; Error in this line
}
let generator = generate();
let question = generator.next().value;
try {
  generator.throw(new Error("데이터베이스에서 답을 찾지 못했습니다."));
}catch (e) {
  alert(e); // 에러 출력
}

이렇게 제너레이터 바깥에서 에러를 잡지 못하면 에러는 제너레이터 호출 코드 밖으로 떨어져 나간다.

요약

  • 제너레이터는 제너레이터 함수 function* f(…) {…}을 사용해 만든다.
  • yield 연산자는 제너레이터 안에 있어야 한다.
  • next/yield 호출을 사용하면 외부 코드와 제너레이터 간에 결과를 교환할 수 있다.

모던 자바스크립트에서는 제너레이터를 잘 사용하지 않는다. 그러나 제너레이터를 사용하면 실행 중에도 제너레이터 호출 코드와 데이터를 교환할 수 있기 때문에 유용한 경우가 종종 있다. 그리고 제너레이터를 사용하면 이터러블 객체를 쉽게 만들 수 있다는 장점이 있다.

Thank You for Visiting My Blog, Have a Good Day 😁
© 2022 Developer Minter, Powered By Gatsby.