[js] Iterator, Generator 정리 (2) - 재사용 가능한 Iterable

vhv3y8·2023년 12월 6일

자바스크립트

목록 보기
2/3

어떤 Iterable은 여러 번 불러도 잘 동작하고 어떤 건 한 번만 쓸 수 있다고 하는데, 어떤 점이 그 둘을 구분하는지 알아보자.

자바스크립트의 여러 문법들(for...of, spread 등등)은 Iterable을 받는다.

그 문법들은 Iterable의 [Symbol.iterator]()를 불러서 Iterator를 받는다.

Iterable은 이터레이션이 가능한 객체를 뜻하고, 그 조건은 Iterator를 줄 수 있는지 여부다.

Iterator는 반복이 될 때 값을 돌려주는 주체이다.

Iterator의 재사용 가능 여부

우선 이터레이터는 무한으로 값을 돌려줄 수도 있고, 유한일 수도 있다.

// 무한
const infinite = {
  i: 0,
  next() {
    return { value: this.i++, done: false };
  },
};

console.log(infinite.next());
// {value: 0, done: false}
console.log(infinite.next());
// {value: 1, done: false}
console.log(infinite.next());
// {value: 2, done: false}
// ...
// 유한
const finite = {
  i: 0,
  next() {
    return this.i < 3
      ? { value: this.i++, done: false }
      : { value: undefined, done: true };
  },
};

console.log(finite.next());
// {value: 0, done: false}
console.log(finite.next());
// {value: 1, done: false}
console.log(finite.next());
// {value: 2, done: false}
console.log(finite.next());
// {value: undefined, done: true}

즉 끝이 날 수도 있고 안날 수도 있다.

하지만 이터레이터가 끝났다면, 그걸 다시 시작하는 건 불가능하다.

새로운 이터레이터를 돌려줘야 한다.

왜 그런지 예시로 알아보자.

const iterable = {
  i: 0,
  next() {
    return this.i < 3
      ? { value: this.i++, done: false }
      : { value: undefined, done: true };
  },
  [Symbol.iterator]() {
    return this;
  },
};
for (let k of iterable) {
  console.log(k);
}
// 0
// 1
// 2

여기서 for...of 루프는 [Symbol.iterator]()를 불러서 Iterator(this)를 받았다.

앞서 적었듯 Iterator가 iteration을 수행하는 주체이고, 이터러블은 Iterator를 돌려줄 수 있는 객체일 뿐이다.

루프가 돌아감에 따라 i 값은 3까지 올라간다.

여기서 iterable을 다시 불러서 루프를 도는 게 가능할까?

for (let k of iterable) {
  console.log(k);
}
// 출력 X

for...of 루프가 [Symbol.iterator]()를 불러서 this를 받았다.

그리고 루프가 돌기 시작하면서 next()를 부르는데, i는 이미 3이어서 {value: undefined, done: true}를 돌려주고 그대로 끝나게 된다.

따라서 정리해보면 이렇다.

next()가 값을 유한하게 돌려준다면, 위 객체의 i 같이 어떤 값에 반드시 의존적일 수밖에 없다.

그래서 이미 iteration이 끝난 Iterator를 다시 사용하는 건 불가능하다.

Generator의 재사용 가능 여부

제너레이터도 마찬가지다.

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

const iterable = generatorFunction();

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

for (let k of iterable) {
  console.log(k);
}
// 출력 X

제너레이터 함수가 돌려주는 객체는 Generator인데(여기서 iterable), 이 객체는 Iterable이자 Iterator이다.

Generator의 next()가 돌려주는 값은 제너레이터 함수의 yield에 의해 결정되고, [Symbol.iterator]()는 자신을 돌려준다고 한다.

this를 돌려준다는거다.

정리해보면 이렇다.

제너레이터의 next()도 제너레이터 함수의 yield에 의존적이다.

따라서 돌려주는 값이 유한하다면, 결국 다 소모되고나면 다시 불러서 사용하는 게 불가능하다.

재사용 가능한 Iterable를 만드려면

결론적으로 같은 이터레이터를 2번 이상 사용하는 게 불가능하다.

여러 번 쓰고 싶다면, Iterable이 불러질 때마다 다른 이터레이터를 돌려줘야한다.

[Symbol.iterator]()가 매번 새로운 이터레이터를 만들어서 줘야 한다는거다.

const iterable = {
  [Symbol.iterator]() {
    let i = 0;
    // Iterator를 만들어서 돌려줌
    return {
      next() {
        return i < 3
          ? { value: i++, done: false }
          : { value: undefined, done: true };
      },
    };
  },
};

여기서 [Symbol.iterator]()는 불러질 때마다 이터레이터 객체를 만들어서 돌려준다.

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

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

따라서 Iterable을 부를 때마다 이터레이터가 다르므로 몇 번이고 다시 쓸 수 있다.

핵심은 [Symbol.iterator]()가 똑같은 이터레이터를 돌려주느냐, 아니면 매번 새 이터레이터를 만들어서 돌려주느냐다.

Iterable 메서드가 돌려주는 객체에 따른, Iterable Iterator의 재사용 가능 여부

다시 정리를 해보자.

같은 Iterable Iterator이더라도, 이터러블이 어떤 객체를 돌려주느냐에 따라 반복 가능 여부가 달라진다.

Iterable Iterator

// 한 번만 사용 가능
const iterableIterator = {
  i: 0,
  next() {
    return this.i < 3
      ? { value: this.i++, done: false }
      : { value: undefined, done: true };
  },
  // 같은 객체를 돌려줌
  [Symbol.iterator]() {
    return this;
  },
};

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

for (let k of iterableIterator) {
  console.log(k);
}
// 출력 X

this로 동일한 객체를 매 번 돌려주기 때문에, 재사용이 불가능하다.

// 여러 번 사용 가능
const iterableIterator = {
  // 매 번 객체를 새로 만들어서 돌려줌
  [Symbol.iterator]() {
    let i = 0;

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

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

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

매번 새 객체를 만들어서 돌려주기 때문에 재사용이 가능하다.

Generator

그리고 제너레이터는 한 번만 쓸 수 있지만, 매 번 제너레이터 함수를 불러주면 여러 번 쓸 수 있다 :

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

const generator = generatorFunction();

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

for (let k of generator) {
  console.log(k);
}
// 출력 X

하나의 제너레이터는 반복이 끝나면 다시 사용할 수 없다.

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

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

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

매 번 제너레이터 함수를 불러서 제너레이터를 생성하면, 그때마다 이처럼 사용이 가능하다.

제너레이터가 자기 자신(this)을 돌려주는 이터레이터라는 사실은 변하지 않는다.

하지만 매 번 generatorFunction()을 써주면 그 이터레이터는 딱 한 번만 쓰이고 끝난다.

부를 때마다 새 제너레이터를 만들어서 주기 때문이다.

결론 : Iterable 메서드를 제너레이터 함수로 구성하기

일반적으로 제너레이터 함수와 제너레이터를 통해서만 Iterator나 Iterable을 만들고 쓸 것이다. 그러면 그걸 어떻게 구성해야할까?

핵심은 결국 이터러블 메서드인 [Symbol.iterator]()가 새로운 이터레이터를 만들어서 돌려줘야하는거다.

그리고 제너레이터 함수는 불러질 때마다 새로운 제너레이터를 만들어서 준다.

따라서 이걸 합쳐서 [Symbol.iterator]제너레이터 함수를 값으로 준다면 여러 번 쓸 수 있는 이터러블이 완성된다.

어쨌든 이터러블 메서드가 매 번 Iterator 객체를 만들며 돌려주면 되므로, 직접 Iterator 객체를 만들면서 돌려줄 수 있을 것이다 :

const iterable = {
  [Symbol.iterator]: function () {
    let i = 0;
    // Iterator 객체를 만들어서 돌려준다
    return {
      next() {
        return i < 3
          ? { value: i++, done: false }
          : { value: undefined, done: true };
      },
    };
  },
};

이렇게 구현해도 여러 번 사용 가능하지만 작성하기 상대적으로 복잡하다.

const iterable = {
  // Generator를 만들어서 돌려주는 제너레이터 함수
  [Symbol.iterator]: function* () {
    for (let i = 0; i < 3; i++) {
      yield i;
    }
  },
};

이터러블 메서드에 제너레이터 함수를 써주면 이터레이터가 돌려줄 값을 더 간편하고 쉽게 적을 수 있다.

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

iterable[Symbol.iterator]()(제너레이터 함수)를 부르고 제너레이터를 받았다.

그리고 이 제너레이터의 next()를 부르면서 각 값을 얻었다.

여기서 제너레이터의 이터러블 메서드인 [Symbol.iterator]()는 쓰이지 않는다.

즉, 제너레이터는 Iterator로서의 역할만 하며, next() 메서드만이 쓰인다.

const iterable = {
  *[Symbol.iterator]() {
    for (let i = 0; i < 3; i++) {
      yield i;
    }
  },
};

method definition으로 줄여서 적으면 이렇게 된다.

그리고 이 메서드 하나만 구현하면 어떤 객체든 간에 자신의 프로퍼티를 돌면서 yield 해주면 바로 iterable하게 되기 때문에 유용하다고 한다.

수정 : 2025-01-24

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

0개의 댓글