[js] Iterator, Generator 정리 (1) - Iterator, Iterable, Generator

vhv3y8·2023년 12월 5일

자바스크립트

목록 보기
1/3

먼저 간단하게 요약해보자면 다음과 같다 :

  • Iterator
    • next() 메서드를 갖고 있는 객체
  • Iterable
    • [Symbol.iterator]() 메서드를 갖고 있는 객체
  • Generator
    • 제너레이터 함수가 돌려주는 Iterable Iterator 객체
  • Generator Function
    • function*으로 시작하는 함수

1. Iterator

next() 메서드를 갖고 있는 객체

const myIterator = {
  next() {
    return { value: 0, done: false };
  },
};

next() 메서드는 객체를 돌려줘야 한다.

이 객체는 valuedone 프로퍼티를 가져야 한다.

value = 돌려줄 값

done = iteration이 끝났는지 여부

{ value: undefined, done: false }

만약 프로퍼티에 값이 없다면, 각각 이 값이 적용된다고 한다.

정리하면, "위와 같은 객체를 돌려주는 next() 메서드"를 가진 객체Iterator라고 한다.

2. Iterable

[Symbol.iterator]() 메서드를 갖고 있는 객체

[Symbol.iterator]() = [@@iterator]() = @@iterator method 모두 같은 말이다.

코드에서 쓸 때는 Symbol.iterator를 쓴다.

const myIterator = {
  next() {
    return { value: 0, done: false };
  },
};

const myIterable = {
  [Symbol.iterator]() {
    return myIterator;
  },
};

[Symbol.iterator]() 메서드는 Iterator 객체를 돌려줘야 한다.

따라서 정리해보면 다음과 같다.

"Iterator를 돌려주는 [Symbol.iterator]() 메서드"를 갖고 있는 객체를 Iterable이라고 한다.

구체적인 예시로 알아보면 이렇다.

const myIterator = {
  i: 0,

  next() {
    return this.i < 3
      ? { value: this.i++, done: false }
      : { value: undefined, done: true };
  },
};

const myIterable = {
  [Symbol.iterator]() {
    return myIterator;
  },
};

myIteratori가 3보다 작을 때 valuei를 돌려주면서 1을 더하고, i가 3이 되면 끝난다.

for (let k of myIterable) {
  console.log(k);
}
// 0
// 1
// 2

spread syntax, for...of 같은 자바스크립트의 많은 문법들은 Iterable을 받는다.

for...of 루프는 Iterable의 [Symbol.iterator]() 메서드를 부르면서 시작한다.

그리고 그 메서드로부터 받은 Iterator 객체로 루프를 돈다.

각 반복마다 그 Iterator의 next() 메서드를 불러서 값을 얻는거다.

3. Iterable Iterator

방금까지의 내용을 정리해보면 다음과 같다 :

next() 메서드를 갖는 객체가 Iterator고, [Symbol.iterator]() 메서드를 갖는 객체가 Iterable이다.

따라서 객체는 Iterator이면서 동시에 Iterable일 수 있다. 그런 객체를 Iterable Iterator라고 이름 붙여보자.

자바스크립트의 여러 문법들은 Iterable을 받으므로, 다음처럼 Iterable Iterator를 쓰면 복잡해질 필요 없이 하나의 객체로 해결할 수 있을 것이다 :

const myIterableIterator = {
  i: 0,

  next() {
    return this.i < 3
      ? { value: this.i++, done: false }
      : { value: undefined, done: true };
  },

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

for (let k of myIterableIterator) {
  console.log(k);
}
// 0
// 1
// 2

따라서 Iterable이 아닌 Iterator는 별로 유용하지 않다고 볼 수 있다.

Iterator를 만든다면 [Symbol.iterator]() 메서드도 구현해주는 게 훨씬 쓸모있다.

4. Generator

제너레이터 함수가 돌려주는 Iterable Iterator 객체

제너레이터는 Iterator에 속한다.

즉, next() 메서드를 갖는 객체이다.

그리고 Iterable이기도 하다.

[Symbol.iterator]() 메서드도 갖는데, 제너레이터 객체 자기 자신을 돌려준다.

따라서 Iterable Iterator라고 할 수 있다.

그리고 제너레이터는 Generator Function을 통해서만 생성된다.

5. Generator Function

function*으로 시작하는 함수

function* myGeneratorFunction() {
  yield 0;
  yield 1;
  yield 2;
}

const myGenerator = myGeneratorFunction();

제너레이터 함수는, 불러지면 일반 함수처럼 내부가 실행되는 게 아니라 Generator 객체를 돌려준다.

참고로 Generator 객체는, 앞서 적었듯 next()를 갖는 Iterator이다.

그리고 그 Generator에서 next()가 불러졌을 때, 제너레이터 함수의 내부가 실행된다.

이 실행은 처음으로 만나는 yield expression까지 계속된다.

yield는 Generator의 next()가 돌려줄 값을 결정한다.

정리해보면 이렇다.

제너레이터 함수의 내부는 자신이 돌려준 Generator의 next()를 부를 때마다 실행되는데, yield를 만날 때까지만 실행된다.

function* myGeneratorFunction() {
  console.log("a");
  yield 0;
  console.log("b");
  yield 1;
  console.log("c");
  yield 2;
  console.log("d");
}

const myGenerator = myGeneratorFunction();

console.log(myGenerator.next());
// a
// {value: 0, done: false}

console.log(myGenerator.next());
// b
// {value: 1, done: false}

다르게 말해, yield는 제너레이터 함수 내부의 실행을 멈추고 재개하는 지점이 된다.

그리고 yield operator 뒤에 오는 expression의 값이, next()가 돌려주는 객체의 value가 된다.

function addOne(num) {
  return num + 1;
}

function* myGenFunc() {
  yield addOne(1) + (2 * 3) / 4;
}

const myGen = myGenFunc();

console.log(myGen.next());
// {value: 3.5, done: false}

결론

따라서 제너레이터 함수는 매우 유용하다.

불렀을 때 만들어서 돌려주는 Generator는 Iterator이자 Iterable이며,

그 Generator가 반복될 때마다 돌려줄 값(value)은 제너레이터 함수 내부에서 yield로 지정해줄 수 있다.

결론적으로, 제너레이터 함수를 쓰면 지금까지 나온 모든 걸 한 번에 할 수 있다.

Iterator와 Iterable을 알아야 어떻게 돌아가는지 이해할 수 있긴 하지만, 실질적으로 사용하는 건 Generator Function이라는 걸 알 수 있다.

예제 및 사용 예시

for (let i of range(0, 3)) {
  console.log(i);
}

이렇게 사용할 수 있는 range() 함수를 Iterable Iterator와 Generator function으로 구현해보자.

// Iterable Iterator를 돌려주는 함수
function range(start, stop) {
  let current = start;

  return {
    next() {
      return current < stop
        ? { value: current++, done: false }
        : { value: undefined, done: true };
    },
    [Symbol.iterator]() {
      return this;
    },
  };
}

for (let i of range(0, 3)) {
  console.log(i);
}
// 0
// 1
// 2
// Generator를 돌려주는 제너레이터 함수
function* range(start, stop) {
  for (let i = start; i < stop; i++) yield i;
}

for (let i of range(0, 3)) {
  console.log(i);
}
// 0
// 1
// 2

제너레이터 함수를 쓰면 코드가 더 짧기도 하지만, 복잡하게 세세한 걸 적을 필요 없이 돌려줄 값만 적으면 된다는 점도 크다.

function* zeroesForever() {
  while (true) yield 0;
}

const zeroGenerator = zeroesForever();

console.log(zeroGenerator.next().value);
// 0
console.log(zeroGenerator.next().value);
// 0
console.log(zeroGenerator.next().value);
// 0

길이가 무한인 배열을 만들 수는 없지만, 이렇게 끝나지 않는 제너레이터는 만들 수 있다.

사용자가 필요한만큼 사용할 수 있다.

참고

수정 : 2025-01-24

profile
개발 기록, 미래의 나에게 설명하기

0개의 댓글