Iterator

이터레이터와 제너레이터는 ES6에 새로 도입된 중요한 개념이다.

이터레이터는 '지금 어디 있는지' 파악할 수 있도록 돕는다는 면에서 일종의 책갈피와 비슷한 개념이다. 배열은 이터러블 객체의 좋은 예로 배열의 여러 요소에 이터레이터를 사용할 수 있다.

const book = [
    '1 page',
    '2 page',
    '3 page',
    '4 page',
    '5 page',
    '6 page',
    '7 page',
  ];

  const it = book.values();    // 이터레이터를 it으로 줄여 씀

  console.log(it.next());
  console.log(it.next());
  console.log(it.next());
  console.log(it.next());
  console.log(it.next());
  console.log(it.next());
  console.log(it.next());
  console.log(it.next());

// {value: "1 page", done: false}
// {value: "2 page", done: false}
// {value: "3 page", done: false}
// {value: "4 page", done: false}
// {value: "5 page", done: false}
// {value: "6 page", done: false}
// {value: "7 page", done: false}
// {value: undefined, done: true}

next() 메서드를 사용하면 배열을 읽기 시작하고, 메서드가 반환하는 객체에는 value 프로퍼티와 done 프로퍼티가 있다. done 프로퍼티는 마지막 요소를 읽으면 true를 반환한다. 이터레이터는 더 진행할 것이 없으면 value는 undefined가 되지만, next는 계속 호출할 수 있다.

it.next()를 호출하는 중간에 다른 일을 할 수 있다.
for...of 의 경우 인덱스 없이 루프를 실행 할 수 있는데 이터레이터가 제공되기 때문이다.

이터레이터와 while 루프를 사용해서 for...of 루프를 흉내 내 보자.


  const it = book.values();

  let current = it.next();
  while(!current.done) {
    console.log(current.value);
    current = it.next();
  }

이터레이터는 모두 독립적이므로 새 이터레이터를 만들 때마다 처음에서 시작한다. 그리고 각각 다른 요소를 가리키는 이터레이터 여러 개를 동시에 사용할 수 도 있다.

const it1 = book.values();
  const it2 = book.values();

  // it1 두 페이지 읽기
  console.log(it1.next()); // {value: "1 page", done: false}
  console.log(it1.next()); // {value: "2 page", done: false}

  // it2 한 페이지 읽기
  console.log(it2.next()); // {value: "1 page", done: false}

  // it1 한 페이지 더 읽기
  console.log(it1.next()); // {value: "3 page", done: false}

it1 과 it2 이터레이터가 서로 독립적이며 같은 배열에서 따로따로 움직일 수 있다는 것을 보여준다.

이터레이터는 무한한 데이터에도 사용할 수 있다.

피보나치 수열은 무한히 계속되고, 프로그램에서는 몇 번재 숫자까지 계산해야 할지 알 수 없으므로 이터레이터를 사용하기에 알맞다. 이전 예제와의 차이점은 이터레이터가 done에서 절대 true를 반환하지 않는다는 것이다.

const Posts = () => {

  class Fibonacci {
    [Symbol.iterator]() {
      let a = 0, b = 1;
      return {
        next() {
          let rval = { value: b, done: false };
          b += a;
          a = rval.value;
          return rval;
        }
      };
    }
  }

for...of 루프로 Fibonacci 인트턴스를 계산하면 무한 루프에 빠지게 되므로, 10회 계산한 뒤 break 문으로 빠져나오게 한다.

const fib = new Fibonacci();
  let i = 0;
  for(let n of fib) {
    console.log(n);
    if(++i > 9) break;
  }

Generator

제너레이터란 이터레이터를 사용해 자신의 실행을 제어하는 함수로, 일반적인 함수는 매개변수를 받고 값을 반환하지만, 호출자는 매개변수 외에는 함수의 실행을 제어할 방법이 전혀 없다. 함수를 호출하면 그 함수가 종료될 때까지 제어권을 완전히 넘기는 것이다.

하지만 제너레이터는 그렇지 않고, 두 가지 새로운 개념을 도입했다.
하나는 함수의 실행을 개별적 단계로 나눔으로써 함수의 실행을 개별적 단계로 나눠 함스의 실행을 제어하는 것.
다른 하나는 실행 중인 함수와 통신한다는 것이다.

제너레이터는 두 가지 예외를 제외하면 일반적인 함수와 같다.

  • 제너레이터는 언제든 호출자에게 제어권을 넘길 (yield) 수 있다.
  • 제너레이터는 호출한 즉시 실행되지는 않는다. 대신 이터레이터를 반환하고, 이터레이터의 next 메서드를 호출함에 따라 실행된다.

제너레이터를 만들 때는 function 키워드 뒤에 애스터리크스(*)를 붙인다.

function* rainbow() {
    yield 'red';
    yield 'orange';
    yield 'yellow';
    yield 'green';
    yield 'blue';
    yield 'indigo';
    yield 'violet';
  }

  const it = rainbow();
  it.next();
  it.next();
  it.next();
  it.next();
  it.next();
  it.next();
  it.next();
  it.next();

rainbow 제너레이터는 이터레이터를 반환하므로 for...of 루프에서 쓸 수 있다.

for(let color of rainbow()) {
    console.log(color);
}

무지개 색이 콘솔에 입력된다.

yield 표현식과 양방향 통신

제너레이터와 호출자 사이에서 양방향 통신은 yield 표현식을 통해 이루어진다. 표현식은 값으로 평가되고 yield는 표현식이므로 반드시 어떤 값으로 평가된다. yield 표현식의 값은 호출자가 제너레이터의 이터레이터에서 next를 호출할 때 제공하는 매개변수이다.

function* interrogate() {
    const name = yield "What is yout name?";
    const color = yield "What is your favorite color?";
    return `${name}'s favorite color is ${color} `;
}

이 제너레이터를 호출하면 이터레이터를 얻는다.
next행을 호출하면 첫 번째 행을 실행하려 하는데 yield 표현식이 들어 있으므로 제너레이터는 반드시 제어권을 호출자에게 넘겨야 한다.

let it = interrogate();
it.next()         // {value: "What is your name?", done: false}
it.next("승진") // {value: "What is your favorite color?", done: false}
it.next("파랑") // {value: "승진's favorite color is 파랑", done: true}

호출자가 제너레이터에게 정보를 전달하므로, 제너레이터는 그 정보에 따라 자신의 동작 방식 자체를 바꿀 수 있어 유용하게 사용할 수 있다.

제너레이터와 return

제너레이터에서 return 문을 사용하면 위치와 관계없이 done은 true가 되고, value 프로퍼티는 return이 반환하는 값이 된다.

function* abc() {
    yield 'a';
    yield 'a';
    return 'c';
}

const it = abc();
it.next();    // { value: 'a', done: false }
it.next();    // { value: 'b', done: false }
it.next();    // { value: 'c', done: true }

제너레이터를 사용할 때는 보통 done이 true일 경우 value 프로퍼티를 무시하게 된다. 예를 들어 이 제너레이터를 for...of 루프에서 사용하면 c는 절대 출력되지 않는다.

for(let l of abc()) {
    console.log(l);
}

마무리

이터레이터로 할 수 있는 일은 ES6 이전에도 모두 할 수 있었으므로, 새로운 기능이 추가된 것은 아니다. 중요하면서도 자주 사용되는 패턴은 표준화했다는 점이 중요하고, 제너레이터를 사용하면 함수를 훨씬 더 유연하고 효율적으로 사용할 수 있다.
이제 함수를 호출하는 부분에서 데이터를 제공하고 호출한 함수가 완료될 때 까지 기다렸다가 반환값을 받는다는 사고방식에 얽매일 필요가 없다.