본문 바로가기

Front-end/JavaScript

[JavaScript] 제너레이터

1. 제너레이터 함수

일반 함수는 하나 혹은 0개의 값을 반환한다. 하지만 제너레이터(generator)를 사용하면 여러 개의 값을 필요에 따라 하나씩 반환 (yield)할 수 있다. 

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

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

//기본구조

제너레이터 함수는 일반 함수와 동작방식이 다른데, 제너레이터 함수를 호출하면 코드가 실행되는게 아니라, 실행을 처리하는 객체인 '제너레이터 객체'가 반환된다.

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

let generator = generateSequence();
alert(generator); // [object Generator]

-> 여기서 함수를 실행시키기 위해서는 next()라는 제너레이터의 메서드가 필요하다. next()를 호출하면 가장 가까운 yield<value>문을 만날 때까지 실행이 지속된다.

*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}

현재는 첫 번째 값만 받았으므로, 함수 실행은 여기서 멈춰있다.

let two = generator.next();

alert(JSON.stringify(two)); // {value: 2, done: false}
//generator.next()를 다시 호출하면 실행이 재개되고 다음 yield를 반환한다.

let three = generator.next();

alert(JSON.stringify(three)); // {value: 3, done: true}

계속해서 generator.next()를 호출하다 return문에 다다르면 함수가 종료된다.

--> 이때 제너레이터는 종료되며, 여기서 더 next()를 호출하면 {done : true}객체만 계속 반환된다.

 

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

제너레이터는 이터러블이다. (반복 가능하다)

따라서 for...of 반복문을 사용해 값을 얻을 수 있다

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

let generator = generateSequence();

for(let value of generator) {
  alert(value); // 1, 2가 출력됨
}

// .next().value을 연속적으로 호출하는 것 보다 나은 방법이다.

-->위 예시를 실행하면 1과 2만 출력되고 3은 출력되지 않는다.

이유는 for..of 이터레이션이 done: true일 때 마지막 value를 무시하기 때문이다.  그러므로 for..of를 사용했을 때 모든 값이 출력되길 원한다면 yield로 값을 반환해야 한다.

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

let generator = generateSequence();

for(let value of generator) {
  alert(value); // 1, 2, 3
}

 

제너레이터는 이터러블 객체이므로 제너레이터에도 스프레드 문법(...) 같은 관련 기능을 사용할 수 있다.

 

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

let sequence = [0, ...generateSequence()];

alert(sequence); // 0, 1, 2, 3

 

3. 제너레이터 컴포지션

제너레이터 컴포지션(generator composition)은 제너레이터 안에 제너레이터를 '임베딩(embedding, composing)'할 수 있게 해주는 제너레이터의 특별 기능이다.

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*를 사용하여 제너레이터를 다른 제너레이터에 끼워넣을 수 잇다는 것!!

더보기

사실 위의 코드는 아래 코드처럼 제러레이터를 직접 써준 코드와 같은 기능을 한다.

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

function* generateAlphaNum() {

  // yield* generateSequence(48, 57);
  for (let i = 48; i <= 57; i++) yield i;

  // yield* generateSequence(65, 90);
  for (let i = 65; i <= 90; i++) yield i;

  // yield* generateSequence(97, 122);
  for (let i = 97; i <= 122; i++) yield i;

}

let str = '';

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

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

 

4. '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()를 호출하면 실행이 시작되고 첫 번째 yield "2+2=?"의 결과가 반환된다. 이 시점에는 제너레이터가 (*)로 표시한 줄에서 실행을 잠시 멈춘다.

2. 그리고, 위 그림에서 보듯이 yield의 결과가 제너레이터를 호출하는 외부 코드에 있는 변수, question에 할당된다.

3. 마지막 줄, generator.next(4)에서 제너레이터가 다시 시작되고 4는 result에 할당된다. (let result = 4)

 

**중요한 건, 외부 코드에서는 next(4)를 즉시 호출하지 않고 있다는 점. 제너레이터가 기다려주기 때문에 호출을 나중에 해도 문제되지 않는다.

 

5. generator.throw

외부코드가 yield의 결과가 될 값을 제너레이터 안에 전달할 수도 있지만, 외부코드가 에러를 만들거나 던질 수도 있다.

일단 에러를 yield 안으로 전달하려면 generator.throw(err)를 호출해야 한다. (이때 err가 yield가 잇는 줄로 던져짐)

//"2 + 2 = ?"의 산출 값이 에러를 발생시키는 경우

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)에서 generator.throw()의 인자로 전달 받은 에러가 제너레이터 안으로 던져지고, yield와 함께 라인 (1)에서 예외를 만들어 alert스크립트가 실행되지 못하게 한다. 예외는 try..catch에서 잡히고, 관련 정보가 얼럿창에 출력된다.

제너레이터 안에서 예외를 처리하지 않았다면 예외는 여타 예외와 마찬가지로 제너레이터 호출 코드(외부 코드)로 떨어져 나오게 된다.