[자바스크립트 완벽가이드] - 이터레이터와 제너레이터

Lee Jeong Min·2022년 6월 12일
2

자바스크립트

목록 보기
13/17
post-thumbnail

자바스크립트 완벽가이드 12장에 해당하는 부분이고, 읽으면서 자바스크립트에 대해 새롭게 알게된 부분만 정리한 내용입니다.

이 장에서는 이터레이터가 어떻게 동작하는지 설명하고 이터러블 데이터 구조를 직접 만드는 방법을 설명한다.

이터레이터의 동작 방법

JS의 순회를 이해하려면 다음 3가지를 이해해야 한다.

  • 이터러블 객체(배열, 세트, 맵 같이 순회할 수 있는 타입의 객체)

    Symbol.iterator라는 이터레이터 메서드를 가진 객체

  • 이터레이터 객체 자체(순회를 수행하는 객체 자체)

    next() 메서드가 있는 객체

  • 순회 결과(iteration result) 객체

    valuedone 프로퍼티가 있는 객체

즉 다시 말하면, 이터러블 객체란 이터레이터 객체를 반환하는 특별한 이터레이터 메서드(Symbol.iterator)를 가진 객체이다. 또한 이터레이터 객체는 순회 결과 객체를 반환하는 next() 메서드가 있는 객체이다.

이터러블 객체 iterable을 순회하는 단순한 for/of 루프 예시

// 객체 생성
const iterable = [99];
// 이터레이터 메서드를 호출하여 이터레이터 객체 생성
const iterator = iterable[Symbol.iterator]();
for (let result = iterator.next(); !result.done; result = iterator.next()) {
  console.log(result.value); // 99
}

이터레이터 객체 그 자체가 이터러블인 경우

const list = [1, 2, 3, 4, 5];
const iter = list[Symbol.iterator]();
const head = iter.next().value;
const tail = [...iter];

console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]

내장된 이터러블 데이터 타입의 이터레이터 객체는 그 자체가 이터러블이다. 이런 특징이 유용할 때가 간혹 있다.

이터러블 객체 만들기

클래스를 이터러블로 만들기 위해서는 반드시 이름이 Symbol.iterator인 메서드를 만들어야 한다. 이 메서드는 반드시 next() 메서드가 있는 이터레이터 객체를 반환해야한다. next() 메서드는 반드시 순회 결과 객체를 반환해야 하며 순회 결과 객체에는 value 프로퍼티와 불 done 프로퍼티 중 하나는 반드시 존재해야 한다.

class Range {
  constructor(from, to) {
    this.from = from;
    this.to = to;
  }

  has(x) {
    return typeof x === 'number' && this.from <= x && x <= this.to;
  }

  toString() {
    return `{x | ${this.from} <= x ${this.to}}`;
  }

  // 이터레이터 객체 반환 -> 이터러블을 만들기 위해서
  [Symbol.iterator]() {
    let next = Math.ceil(this.from);
    const last = this.to;
    return {
      // next() 메서드가 이터레이터 객체의 핵심
      next() {
        return next <= last ? { value: next++ } : { done: true };
      },

      [Symbol.iterator]() {
        return this;
      }
    };
  }
}

for (const x of new Range(1, 10)) console.log(x); // 1부터 10까지 숫자
console.log(...new Range(-2, 2)); // -2 -1 0 1 2

이터러블 객체와 이터레이터 핵심 특징 중 하나는 이들이 본질적으로 느긋하다(lazy)는 것이다. 따라서 그 값이 실제 필요할 때까지 계산을 늦추어 메모리를 아낄 수 있다.

이터레이터 '종료': return 메서드

이터레이터 객체에 종료를 위해 return() 메서드가 사용되기도 한다.

next()가 done 프로퍼티가 true인 순회 결과를 반환하기 전에 순회를 마쳐야 한다면 인터프리터는 이터레이터 객체에 return() 메서드가 있는지 확인한다.

제너레이터

function* 키워드를 사용하여 정의한다. 이 함수를 호출하면 제너레이터 객체를 반환한다.

function* oneDigitPrimes() {
  yield 2;
  yield 3;
  yield 5;
  yield 7;
}

// 제너레이터 함수를 호출하여 제너레이터를 생성한다.
const primes = oneDigitPrimes();

console.log(primes.next().value); // 2
console.log(primes.next().value); // 3
console.log(primes.next().value); // 5
console.log(primes.next().value); // 7
console.log(primes.next().done); // true

// 제너레이터는 다른 이터러블 타입처럼 사용할 수 있다.
console.log([...oneDigitPrimes()]); // [2, 3, 5, 7]

표현식으로 제너레이터 정의는 가능하지만 화살표 함수 문법은 불가능하다. 또한 제너레이터를 사용하면 아래와 같이 이터러블 클래스를 만들기 쉽다.

// Range 클래스에서 제너레이터를 사용하지 않은 경우
  // 이터레이터 객체 반환 -> 이터러블을 만들기 위해서
  [Symbol.iterator]() {
    let next = Math.ceil(this.from);
    const last = this.to;
    return {
      // next() 메서드가 이터레이터 객체의 핵심
      next() {
        return next <= last ? { value: next++ } : { done: true };
      },

      [Symbol.iterator]() {
        return this;
      }
    };
  }

// Range 클래스에서 제너레이터를 시용한 경우
  *[Symbol.iterator]() {
    for(let x = Math.ceil(this.from); x <= this.to; x++) yield x;
  }

제너레이터를 활요한 피보나치 수열

function* fibonacciSequence() {
  let x = 0;
  let y = 1;
  for (;;) {
    yield y;
    [x, y] = [y, x + y];
  }
}

function fibonacci(n) {
  for (const f of fibonacciSequence()) {
    if (n-- <= 0) return f;
  }
}

console.log(fibonacci(20)); // 10946

// 무한한 제너레이터를 take() 제너레이터와 함께 사용한 경우
function* take(n, iterable) {
  // 이터레이터 객체 생성
  const it = iterable[Symbol.iterator]();
  while (n-- > 0) {
    const next = it.next();
    if (next.done) return;
    yield next.value;
  }
}

console.log([...take(5, fibonacciSequence())]); // 1 1 2 3 5

yied*와 재귀 제너레이터

yield* 키워드는 yield와 비슷하지만 값 하나를 전달하는 것이 아니라 이터러블 객체를 순회하면서 각각의 값을 전달한다.

function* oneDigitPrimes() {
  yield 2;
  yield 3;
  yield 5;
  yield 7;
}

function* sequence(...iterables) {
  for (const iterable of iterables) {
    yield* iterable;
  }
}

console.log([...sequence('abc', oneDigitPrimes())]); // ['a', 'b', 'c', 2, 3, 5, 7]

하지만 아래처럼배열요소를 순회하기 위해 forEach() 메서드를 사용하는 경우 정상적으로 작동이 되지 않는다.

function* sequence(...iterables) {
  iterables.forEach(iterable => yield* iterable); // 에러
}

위 예제의 중첩된 화살표 함수는 일반적인 함수이므로 yield yield*는 제너레이터 함수 안에서만 사용할 수 있으므로 허용되지 않는다.
yield*을 사용해 재귀 제너레이터를 만들수 있고, 재귀적으로 정의된 트리구조에 비재귀적 순회를 수행할 수 있다.

고급 제너레이터 기능

제너레이터 함수의 반환값

제너레이터 함수도 다른 함수와 마찬가지로 값을 반환할 수 있다.

function* oneAndDone() {
  yield 1;
  return 'done';
}

console.log([...oneAndDone()]);

const generator = oneAndDone();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 'done', done: true }
console.log(generator.next()); // { value: undefined, done: true }

next()를 마지막으로 호출했을 때 반환하는 객체에는 value와 done이 모두 존재한다.

yield 표현식의 값

yield는 표현식이라서 값을 가질 수 있다.

function* smallNumbers() {
  console.log('next()가 처음 호출되었으며 인자는 무시됩니다.');
  const y1 = yield 1; // y1 === 'b'
  console.log(`next()가 두 번째로 호출됐으며 인자는 ${y1}입니다.`);
  const y2 = yield 2; // y2 === 'c'
  console.log(`next()가 두 번째로 호출됐으며 인자는 ${y2}입니다.`);
  const y3 = yield 3; // y3 === 'd'
  console.log(`next()가 두 번째로 호출됐으며 인자는 ${y3}입니다.`);
  return 4;
}

const g = smallNumbers();
console.log('제너레이터가 생성됐습니다. 아직 실행된 코드는 없습니다.');
const n1 = g.next('a'); // n1.value = 1;
console.log(`제너레이터가 전달한 값은 ${n1.value}입니다.`);
const n2 = g.next('b'); // n2.value = 2;
console.log(`제너레이터가 전달한 값은 ${n2.value}입니다.`);
const n3 = g.next('c'); // n3.value = 3;
console.log(`제너레이터가 전달한 값은 ${n3.value}입니다.`);
const n4 = g.next('d'); // n4 === {value: 4, done: true}
console.log(`제너레이터는 ${n4.value}를 넘기고 종료됐습니다.`);

제너레이터의 next() 메서드를 다음에 호출할 때 next()에 전달된 인자는 멈췄던 yield 표현식의 값이 된다.
즉, 호출자는 next()를 통해 제너레이터에 값을 전달한다. 첫 번째 전달 값은 무시된다.

제너레이터의 return()과 throw() 메서드

next()뿐만 아니라 return()throw() 메서드를 호출해서 제너레이터의 실행 흐름을 제어할 수 있다.

제너레이터에서는 try/finally 문을 통해 제너레이터가 종료될 때(finally 블록에서) return()을 사용하여 필요한 정리 작업을 수행하게 만들 수 있다. throw()도 마찬가지로 임의의 신호를 예외의 형태로 제너레이터에 보내 예외 처리를 할 수 있다.

제너레이터가 yield*를 통해 다른 이터러블 객체에 값을 전달하면 제너레이터의 next() 메서드를 호출할 때 이터러블 객체의 next() 메서드가 호출된다. return()throw() 메서드도 마찬가지이다. 제너레이터가 return()throw() 메서드가 정의된 이터러블 객체에 yield*를 사용하면, 제너레이터에서 return()이나 throw()를 호출할 때 이터레이터의 return()이나 throw() 메서드가 이어서 호출된다.

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글