본문 바로가기
javascript

ES6의 Iterator와 Generator

by 초특급하품 2019. 10. 16.

이번 글에는 Iterator와 Generator의 스펙과, 왜 등장했는지, 어떻게 사용하는지에 대해서 정리해본다.

Iterator

Iterator는 ES6에 추가되었다. 다시 해석하면 그 전에도 어찌어찌 구현하던 것을 Iterator로 바꿀 수 있단 뜻이다.

Iterator는 정의된 인터페이스를 구현하면 그 구현체를 순회할 수 있는 객체이다.

 

순회라고 하면 Array가 떠오르고, Array를 순회하려면 forEachfor-in으로도 가능하다.

하지만 forEach 는 순회 중간에 중단할 수 없는 단점이 있고, for-in은 이름부터 원하는 순회를 하는 듯 하지만 역시 아래와 같은 여러 단점이 있다.

  1. for (const i in arr) 에서 i의 type이 string으로 뭔가 이상하다.
  2. 왜 string가 하니 arr에 추가 속성이 있는 경우 i가 추가 속성까지 순회한다.
  3. 그마저도 순서가 보장되지 않는다.

이런 문제가 있어서 for-of가 등장했다. for-offor-in의 단점을 보완하고 순회하는 대상을 Array에서 더 나아가 Iterator를 지원한다.

 

이제 Iterator가 나왔다. Iterator를 제공하는 것은 뭔지 그 구현체를 보자.

const countIterator = {
  [Symbol.iterator]: function () {
    return this;
  },
  cnt: 0,
  next: function () {
    return {done: false, value: ++this.cnt};
  }
};

Iterator는 [Symbol.iterator]()를 시작으로 next() method가 정의된다. next()donevalue 속성을 가진다. done은 boolean type으로 해당 Iterator는 true가 나올때 까지 반복된다.

 

countIterator는 항상 {done: false}를 반환하기 때문에 무한 반복된다.
첫 예제부터 Array로는 어떻게 구현해야 할지 어려운 무한 반복되는 Iterator를 구현했다. 뭔가 대단하다.

 

무한반복이 아닌 10까지 카운트하는 동작을 원하면 원하는 시점에 {done: true}를 반환하도록 next를 수정하면 된다.

next: function () {
  return {done: this.cnt >= 10, value: ++this.cnt};
}

 

{done: true}가 나올 때까지 계속 next를 호출해주는 for-of에서 돌려보면 10까지 출력하는 것을 확인할 수 있다.

for(const cnt of countIterator) {
  console.log(cnt); // 1 2 3 4 5 6 7 8 9 10
}

 

Iterator를 살펴봤는데 가장 중요한 점은 위 카운터 예제에서도 알 수 있지만 next()가 수행되기 전까지 현재의 context(여기서는 cnt)를 유지한다는 점이다. context를 유지하며 실행/중지를 반복하여 Lazy Evaluation을 가능하게 하며 이 점을 이용해서 더 멋진 코딩을 할 수 있다.

Generator

Generator는 함수와 비슷하게 정의하지만 호출하면 Iterator 객체가 반환되어 next() method로 제어하게 된다.

Iterator가 반환된다는 것은 [Symbol.iterator]()next()가 구현되었다는 뜻이고, 이걸 javascript가 변환해주려면 당연히 Generator를 문법에 맞게 선언해야 한다.

function* countGenerator(max) {
  for (let i = 1; i <= max; i++)
    yield i;
}

 

위에 Iterator로 구현한 카운터와 똑같은데 Generator로 구현하니 훨씬 간결해졌다.
Iterator로 변환되기 위한 Generator의 문법은 다음과 같다.

  1. function*로 시작한다.
  2. yieldnext()value를 대신한다. 이때 donefalse이다.
  3. return xnext(){value: x, done: true}를 대신한다.

함수의 종료는 return undefined와 같기 때문에 3번 조건을 위해 별도의 코딩은 없어도 된다.

따라서 위에 직관적인 3줄의 코드로 Generator문법으로 Iterator를 만들었으니 실제 사용할 때도 Iterator와 같은 방법으로 쓰면 된다.

const counter10 = countGenerator(10);

for(const cnt of counter10) {
  console.log(cnt); // 1 2 3 4 5 6 7 8 9 10
}

Generator로 구현했지만 역시 context를 유지하는 Iterator로 변환된 것을 확인할 수 있다.

yield의 추가 기능

yieldnext().value를 대신해주는 것 외에도 추가 기능이 있다.

  • next()yield의 반환 값을 넘길 수 있다.
function* countGenerator(max) {
  for (let i = 1; i <= max; i++) {
    const decrease = yield i;
    if(decrease === true) i -= 2;
  }
}

const counter10 = countGenerator(10);
counter10.next(); // {value: 1, done: false}
counter10.next(); // {value: 2, done: false}
counter10.next(true); // {value: 1, done: false}
  • yield*로 또 다른 Iterator/Generator를 실행시킬 수 있다.
function* concatGenerator(counterA, counterB){
  yield* counterA;
  yield* counterB;
}

const counter3 = countGenerator(3);
const counter5 = countGenerator(5);
const concatCounter = concatGenerator(counter3, counter5);

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

Generator를 이용한 Permutation 구현

위에서 익힌 Generator와 yield*를 이용해서 permutation도 만들 수 있다.

function permutation(arr) {
  const n = arr.length, visited = Array(n), result = Array(n);
  return function* _permutation(cur) {
    for(let i = 0; i < n; i++) {
      if(visited[i]) continue;
      visited[i] = true;
      result[cur] = arr[i];
      yield* _permutation(cur + 1);
      visited[i] = false;
    }
    if(cur == n) yield result;
  }(0);
}

for(const perm of permutation([1,2,3])) {
  console.log(perm)
}
/*
  [1, 2, 3]
  [1, 3, 2]
  [2, 1, 3]
  [2, 3, 1]
  [3, 1, 2]
  [3, 2, 1]
*/

'javascript' 카테고리의 다른 글

React class vs function component 차이점  (0) 2020.05.17
Typescript 타입 정의 파일  (0) 2020.03.29
CommonJS / ES 모듈 로딩 방식  (1) 2020.03.29
ES8의 async와 await  (0) 2019.10.16
ES6의 Promise  (0) 2019.10.16

댓글