이터레이터와 제너레이터

이터레이터와 제너레이터 (Iterators and generators)

이터레이터(Iterators)와 제너레이터(Generators)는 반복(iteration)의 개념을 핵심 언어로 직접 가져오고, for...of 루프의 동작을 사용자 정의하는 메커니즘을 제공한다.

자세한 내용은 다음을 참조하라: Iteration protocols, for...of, function*Generator, yieldyield*.

출처: Iterators and generators - JavaScript | MDN

본문

이터레이터 (Iterators)

JavaScript에서 **이터레이터(iterator)**는 시퀀스를 정의하고 잠재적으로 종료 시 반환 값을 가지는 객체다.

구체적으로, 이터레이터는 두 속성을 가진 객체를 반환하는 next() 메서드를 가짐으로써 Iterator 프로토콜을 구현하는 어떤 객체든 된다.

  • value — 반복 시퀀스의 다음 값.
  • done — 시퀀스의 마지막 값이 이미 소비되었으면 true. done과 함께 value가 있으면 그것은 이터레이터의 반환 값이다.

일단 만들어지면 이터레이터 객체는 next()를 반복적으로 호출해 명시적으로 반복할 수 있다. 이터레이터를 반복하는 것을 이터레이터를 **소비(consume)**한다고 한다. 일반적으로 한 번만 가능하기 때문이다. 종료 값이 생성된 후 추가로 next()를 호출하면 계속 {done: true}를 반환해야 한다.

JavaScript에서 가장 흔한 이터레이터는 Array 이터레이터로, 관련 배열의 각 값을 순서대로 반환한다.

모든 이터레이터가 배열로 표현될 수 있다고 상상하기 쉽지만, 실제로는 그렇지 않다. 배열은 전체가 할당되어야 하지만 이터레이터는 필요할 때만 소비된다. 따라서 이터레이터는 0과 Infinity 사이의 정수 범위 같은 무한 크기의 시퀀스도 표현할 수 있다.

다음은 정확히 그렇게 하는 예제다. start(포함)부터 end(제외)까지 step 간격으로 정수 시퀀스를 정의하는 range 이터레이터를 만들 수 있게 한다. 최종 반환 값은 변수 iterationCount가 추적하는, 생성된 시퀀스의 크기다.

function makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let nextIndex = start;
  let iterationCount = 0;

  const rangeIterator = {
    next() {
      let result;
      if (nextIndex < end) {
        result = { value: nextIndex, done: false };
        nextIndex += step;
        iterationCount++;
        return result;
      }
      return { value: iterationCount, done: true };
    },
  };
  return rangeIterator;
}

이터레이터를 사용하는 것은 다음과 같다.

const iter = makeRangeIterator(1, 10, 2);

let result = iter.next();
while (!result.done) {
  console.log(result.value); // 1 3 5 7 9
  result = iter.next();
}

console.log("Iterated over sequence of size:", result.value); // [5 numbers returned, that took interval in between: 0 to 10]

참고: 특정 객체가 이터레이터인지 반사적으로 알아내는 것은 불가능하다. 그렇게 해야 한다면 Iterables를 사용하라.

제너레이터 함수 (Generator functions)

커스텀 이터레이터는 유용한 도구지만, 그 내부 상태를 명시적으로 유지해야 하므로 생성에 신중한 프로그래밍이 필요하다. **제너레이터 함수(Generator functions)**는 강력한 대안을 제공한다: 실행이 연속적이지 않은 단일 함수를 작성함으로써 반복 알고리즘을 정의할 수 있게 해준다. 제너레이터 함수는 function* 문법으로 작성된다.

호출되면 제너레이터 함수는 처음에 코드를 실행하지 않는다. 대신 Generator라는 특수한 유형의 이터레이터를 반환한다. 제너레이터의 next 메서드를 호출해 값이 소비되면, Generator 함수는 yield 키워드를 만날 때까지 실행된다.

함수는 원하는 만큼 호출될 수 있고, 매번 새 Generator를 반환한다. 각 Generator는 한 번만 반복될 수 있다.

이제 위의 예제를 적응시킬 수 있다. 이 코드의 동작은 동일하지만 구현을 쓰고 읽기가 훨씬 쉽다.

function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let iterationCount = 0;
  for (let i = start; i < end; i += step) {
    iterationCount++;
    yield i;
  }
  return iterationCount;
}

Iterables (반복 가능 객체)

객체가 자신의 반복 동작(예: for...of 구성에서 어떤 값이 반복되는지)을 정의하면 그 객체는 **iterable(반복 가능)**하다. ArrayMap 같은 일부 내장 타입은 기본 반복 동작을 가진 반면, Object 같은 다른 타입은 그렇지 않다.

iterable이 되려면 객체가 [Symbol.iterator]() 메서드를 구현해야 한다. 이는 객체(또는 그 프로토타입 체인 위쪽의 객체 중 하나)가 Symbol.iterator 키를 가진 속성을 가져야 한다는 뜻이다.

iterable은 한 번 이상 반복될 수도 있고 한 번만 반복될 수도 있다. 어느 쪽인지 아는 것은 프로그래머에게 달려 있다.

한 번만 반복할 수 있는 iterable(예: Generator)은 관례적으로 [Symbol.iterator]() 메서드에서 this를 반환한다. 반면 여러 번 반복될 수 있는 iterable은 [Symbol.iterator]()를 호출할 때마다 새 이터레이터를 반환해야 한다.

function* makeIterator() {
  yield 1;
  yield 2;
}

const iter = makeIterator();

for (const itItem of iter) {
  console.log(itItem);
}

console.log(iter[Symbol.iterator]() === iter); // true

// 이 예제는 generator(iterator)가 iterable 객체임을 보여준다.
// [Symbol.iterator]() 메서드가 `iter`(자기 자신)를 반환하고,
// 결과적으로 `iter` 객체는 딱 _한 번만_ 반복될 수 있다.

// `iter`의 [Symbol.iterator]() 메서드를 새 iterator/generator 객체를
// 반환하는 함수/generator로 바꾸면 `iter`는 여러 번 반복될 수 있다

iter[Symbol.iterator] = function* () {
  yield 2;
  yield 1;
};

사용자 정의 iterables (User-defined iterables)

다음과 같이 자신만의 iterable을 만들 수 있다.

const myIterable = {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  },
};

사용자 정의 iterable은 평소처럼 for...of 루프나 스프레드 문법에서 사용될 수 있다.

for (const value of myIterable) {
  console.log(value);
}
// 1
// 2
// 3

[...myIterable]; // [1, 2, 3]

내장 iterables (Built-in iterables)

String, Array, TypedArray, Map, Set은 모두 내장 iterable이다. 그 프로토타입 객체들이 모두 Symbol.iterator 메서드를 가지기 때문이다.

iterable을 기대하는 문법들 (Syntaxes expecting iterables)

일부 문장과 표현식은 iterable을 기대한다. 예: for...of 루프, 스프레드 문법, yield*, 구조 분해 문법.

for (const value of ["a", "b", "c"]) {
  console.log(value);
}
// "a"
// "b"
// "c"

[... "abc"];
// ["a", "b", "c"]

function* gen() {
  yield* ["a", "b", "c"];
}

gen().next();
// { value: "a", done: false }

[a, b, c] = new Set(["a", "b", "c"]);
a;
// "a"

고급 제너레이터 (Advanced generators)

제너레이터는 생성된 값을 요청 시(on demand) 계산하므로, 계산 비용이 많이 드는 시퀀스(또는 위에서 보여준 무한 시퀀스까지도)를 효율적으로 표현할 수 있다.

next() 메서드는 값을 받을 수도 있는데, 이 값은 제너레이터의 내부 상태를 수정하는 데 사용될 수 있다. next()에 전달된 값은 yield가 받는다.

참고: 첫 번째 next() 호출에 전달된 값은 항상 무시된다.

다음은 next(x)를 사용해 시퀀스를 재시작하는 fibonacci 제너레이터다.

function* fibonacci() {
  let current = 0;
  let next = 1;
  while (true) {
    const reset = yield current;
    [current, next] = [next, next + current];
    if (reset) {
      current = 0;
      next = 1;
    }
  }
}

const sequence = fibonacci();
console.log(sequence.next().value); // 0
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
console.log(sequence.next().value); // 3
console.log(sequence.next().value); // 5
console.log(sequence.next().value); // 8
console.log(sequence.next(true).value); // 0
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2

제너레이터의 throw() 메서드를 호출하고 그것이 던져야 할 예외 값을 전달해 제너레이터가 예외를 던지도록 강제할 수 있다. 이 예외는 현재 중단된 제너레이터 컨텍스트에서 던져지며, 마치 현재 중단된 yieldthrow 값 문장인 것처럼 동작한다.

예외가 제너레이터 안에서 잡히지 않으면 그것은 throw() 호출을 통해 위로 전파되고, 이후 next() 호출은 done 속성이 true가 되게 한다.

제너레이터에는 주어진 값을 반환하고 제너레이터 자체를 끝내는 return() 메서드가 있다.

더 알아보기