본문으로 바로가기

for...of 이건 되고 이건 왜 안됨?

iterable, iterator에 대해 직접적으로 공부하면 감이 잘 안 옵니다. 우선, javascript의 여러 for문을 살펴보도록합시다.

고전적 for문과 es6 이상부터 가능해진 for...of 와 for... in은 비슷해보이지만 차이가 존재합니다.

간단하게 정리하자면, 

 

  • 고전적 for문 : length 측정 후 하나씩 looping
  • for...in : 객체를 순회합니다. 
  • for...of : iterable을 looping. (한국어판에서는 '순회 가능한 객체'라고 번역해놨는데 오히려 더 헷갈림. 이터러블이라고 부르자)
const arr = [1, 2, 3];

// 고전적인 for문. length에 의존하여 index를 올리는 방식
for (let i = 0; i < arr.length; i++) {
  const element = arr[i];
  console.log(element);
}

// for in 문. iterates over all enumerable properties of an object that are keyed by strings
for (const key in arr) {
  if (Object.hasOwnProperty.call(arr, key)) {
    const element = arr[key];
    console.log(element);
  }
}

// for of 문. loop iterating over iterable objects,
// iterable을 받아서 [Symbol.iterator] 메서드를 실행한 후 {next, value}에서 value를 연달아 반환
for (const value of arr) {
  console.log(value);
}

 

음 좋다. for...of에서는 iterable을 순회한다는 것을 알았다. 

이 iterable에 속하는 것으로, String, Array, 유사 배열, TypedArray, Map, Set, 그리고 유저가 직접 정의한 iterable가 속한다.

String, Array와 같이 애초에 iterable인 객체들은 built-in iterable이라고 부른다.

여튼, 그래서 iterable이 아닌 단순 객체를 for...of에 사용하려고 하면 iterable이 아니라는 에러가 발생합니다.

obj는 iterable이 아니라서 for...of를 사용할 수 없다.

 

그렇다면 iterable은 어떤 것이길래, object는 iterable이 아닌 것인가? 여기에는 명확한 정의가 있다.

 

 

iteration => iterable, iterator

우선, iterable과 iterator를 합쳐서 iteration이라고 부르는데 

여기에 해당하는 문서와 조건(프로토콜)에 대해서는 아래 문서를 참고해야 한다.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols

 

위 문서를 정리하자면, 아래와 같다. (당연히, 요약은 정확하지 않으니 명확한 정의는 직접 문서를 봐야 함)

  • iterable의 조건 : Symbol.iterator 메서드를 가지고 있는 객체. 정확히는 @@iterator 메소드를 구현하는 객체.
    •  Symbol.iterator는 iterator 객체를 반환함. 
  • iterator의 조건 : next 메서드를 가지고 있음
    • next 메서드는 { value, done } 꼴의 객체를 반환함.

 

그렇다면, built-in iterable인 array에도 Symbol.iterator 메서드가 구현되어 있을 것이라고 예측할 수 있습니다.

const arr = [1, 2, 3];
console.log(arr[Symbol.iterator]); // [Function: values]

 

iterator를 반환하지 못하도록, Symbol.iterator 메서드를 null로 지워버리면, arr가 iterable이 아니라는 에러 메세지를 확인할 수 있습니다.

// 브라우저 환경에서만 에러가 남. node 환경에선 에러 안 남.

const arr = [1, 2, 3];

arr[Symbol.iterator] = null;

for (const value of arr) {
  console.log(value); // Error! arr is not iterable
}

 

Symbol.iterator 메서드가 iterator를 반환하니 아래처럼 iterator를 꺼낸 후에 next()를 호출할 수도 있겠죠.

const arr = [1, 2, 3]; // iterable

const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

 

next로 원소 하나를 소비 한 후에 for...of를 돌릴 수도 있습니다.

이유는, Symbol.iterator에 의해 반환된 iterator 내부에 또 Symbole.itertor 메서드가 존재하기 때문입니다.

즉, iterator이면서 동시에 iterable일수도 있다는 것입니다! 이렇게 구성된 iterator를 well-formed iterable라고 부릅니다.

const arr = [1, 2, 3]; // iterable

const iterator = arr[Symbol.iterator]();
iterator.next();

for (const a of iterator) {
  console.log(a);
}

 

built-in iterable인 Map도 아래처럼 for...of를 사용할 수 있겠죠.

const map = new Map([
  ['a', 1],
  ['b', 2],
]);

for (const [key, value] of map) {
  console.log(key, value);
}

// map.keys()의 결과물도 결국 iterable이기 때문에 for...of를 돌릴 수 있음
for (const key of map.keys()) {
  console.log(key);
}

 

 

custom iterable 제작하기 + well formed iterable

 

iterable, iterator의 프로토콜만 지킨다면, 아래와 같은 꼴의 iterable을 스스로 만들어볼 수도 있겠죠. 

생각보다 간단합니다!

const customIterable = {
  // Symbol.iterator 메서드는 iterator를 반환해야 함
  [Symbol.iterator]() {
    let i = 0;
    return {
      // iterator는 next 메서드가 있어야 하며 {value, done} 이 필요함.
      next() {
        if (i > 3) return { value: i, done: true };
        i++;
        return { value: i, done: false };
      },
    };
  },
};

for (const i of customIterable) {
  console.log(i);
}

 

 

그런데 위 custom iterable은 for...of 문으로 동작하긴 하지만, 중간에 iterator를 추출한 후에 다시 for문으로 돌리기 위해서

iterator인 동시에 iterable인 것이 좋습니다. 즉, well-formed iterable여야 한다는 것이죠.

 

위 customIterable을 well-formed iterable로 만들어소 중간에 iterator의 next로 한 번 끊고 for...of를 적용할 수 있도록 만듭시다.

const customIterable = {
  // Symbol.iterator 메서드는 iterator를 반환해야 함
  [Symbol.iterator]() {
    let i = 0;
    return {
      // iterator는 next 메서드가 있어야 하며 {value, done} 이 필요함.
      next() {
        if (i > 3) return { value: i, done: true };
        i++;
        return { value: i, done: false };
      },

      // iterator이자 iterable인 well-formed iterable
      [Symbol.iterator]() {
        return this; // 자기 자신을 반환
      },
    };
  },
};

const iter = customIterable[Symbol.iterator]();
console.log(iter.next());

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

 

여담

 

순회할 수 있는 객체들은, iterable, iterator, well-formed iterable의 프로토콜을 잘 따르는 것이 좋다.

심지어 전개 연산자도 이러한 프로토콜을 잘 따르고 있다. 패키지 만들거면 이런 점들을 잘 고려해서 만들도록 하자.

 

 

MDN 문서 ref)

iteration과 iterable, iterator 프로토콜 : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols

for...of : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of

for...in : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in


darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘 / 전체