Iteration과 Symbol의 개념에 대해서는 이전에도 접한 적이 있다. 그 땐 그냥 '뭐 이런 게 있구만' 하고 넘어갔는데, React에서 상태 관리를 할 때에 iteration에 대해 알고 있어야 했기 때문에 꼭 짚고 넘어가야하는 개념이다.
오늘은 JS ES6의 iteration에 대해 알아보자.
이터레이션 프로토콜은 데이터를 순회하기 위한 목적으로 만들어진 프로토콜이다. 프로토콜은 사전에 약속된 규칙이랄까, 규약이라고도 할 수 있겠다. 이전에 알아 보았던 HTTP(HyperText Transfer Protocol)이 인터넷 통신 규약이란 의미였으니 Iteration Protocol은 순회 규약이라는 의미로 해석할 수 있겠다.
이터레이션 프로토콜은 두 가지의 프로토콜으로 구성되어 있다. 이터러블 프로토콜과 이터레이터 프로토콜이다.
앞으로 나올 용어들은 거의 다 이터 뭐시깽이니까 혼동하지 않도록 주의해야 한다.
MDN에서는 이터러블 프로토콜에 대해 이렇게 설명하고 있다.
iterable protocol 은 JavaScript 객체들이, 예를 들어 for..of 구조에서 어떠한 value 들이 loop 되는 것과 같은 iteration 동작을 정의하거나 사용자 정의하는 것을 허용합니다.
이터레이블 프로토콜을 준수하는 객체를 이터러블 객체라고 한다. 이터레이블 객체는 이터레이블한 객체에만 사용이 가능한 메서드(예를 들어 for..of)나 프로퍼티등을 사용할 수 있게 된다.
그렇다면 어떤 객체가 이터러블 프로토콜을 준수한 객체인가?
바로 [Symbol.iterator]를 자신의 프로퍼티로 가지고 있는(prototype도 포함) 객체가 이터러블 객체이다.
[Symbol.iterator](이하 @@iterator)는 object를 반환하고, arguments를 받지 않으며 이터레이터 프로토콜을 따르는 함수이다.
그렇다면 이터레이터 프로토콜은 무엇일까?
MDN에서는 이터레이터 프로토콜에 대해 이렇게 설명하고 있다.
iterator protocol 은 value( finite 또는 infinite) 들의 sequence 를 만드는 표준 방법을 정의합니다.
객체가 next() 메소드를 가지고 있고, 아래의 규칙에 따라 구현되었다면 그 객체는 iterator이다.
요컨대 이터레이터 프로토콜은 어떠한 value들의 배열을 만드는 표준 방법에 대한 규약이라는 모양이다.
또한 객체가 next() 메소드를 가졌다면 그 객체를 이터레이터라고 한다.
next() 메소드는 아래의 규칙
- object를 반환하며 arguments를 받지 않는다.
- object는 value와 done으로 구성되어 있다.
- value는 Iterator가 반환하는 모든 JS 값이며 done이 true라면 생략될 수 있다.
- done은 Iterator가 마지막 반복 작업을 마쳤을 경우 true. 만약 iterator에 return 값이 있다면 value의 값으로 지정된다.
Iterator의 작업이 남아있을 경우 false. Iterator에 done 프로퍼티 자체를 특정짓지 않은 것과 동일하다.
을 따라야 한다.
설명만으로는 이해하기 힘드니 코드를 살펴보자.
const array = [1, 2, 3];
const arrayIterator = array[Symbol.iterator]();
arrayIterator.next();
arrayIterator.next();
arrayIterator.next();
arrayIterator.next();
String, Array, TypedArray, Map, Set등은 내장 이터러블이다. 이들은 처음부터 @@iterator 프로퍼티를 가진다.
배열을 생성해서 array 변수에 담았다. 그리고 array.Symbol.iterator 메소드를 사용해서 array의 iterator를 반환받아 arrayIterator 변수에 담았다.
그리고 arrayIterator.next()를 실행하니 value와 done으로 구성된 object를 반환하고 있다. value와 done은 위에 설명한 규칙을 잘 따르고 있다. 그러므로 arrayIterator는 이터레이터 프로토콜을 잘 따르고 있고, 이터레이터이다.
사실 나는 여기까지 읽고 이걸 사용해야할 필요를 느끼지 못 했다. 이게 왜 필요한 걸까?
이에 대해서 알아보다 이웅모 님의 블로그에서 잘 정리된 설명을 찾게 되었다.
앞서 이터레이블 객체들은 for...of, spread연산자, 비구조화 할당, Map, Set, Promise.all 등의 API를 사용 할 수 있다고 설명하였다. 바꿔 말하면 이 많은 API들은 이터레이블 객체에만 호환된다. 이것을 '데이터 소비자'라고 이야기 할 수 있다.
또한 앞서 JS에는 내장 이터러블(Built-in Iterable)이 존재한다고 설명했다. JS에 존재하는 내장 이터러블은 다음과 같다.
Array, String, Map, Set, TypedArray(Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array), DOM data structure(NodeList, HTMLCollection), Arguments
내 생각에 아마 내장 이터러블 객체들은 요소들을 순회할 필요가 있다고 판단되는 객체들으로 구성된 것 같다.
이렇게 다양한 데이터 소스들이 모두 자신만의 순회 방식을 갖게 되면 위에 설명한 데이터 소비자들은 각각의 순회 방식을 모두 지원해야 한다. 하지만 이 다양한 데이터 소스들이 이터레이션 프로토콜을 따른다면 데이터 소비자들은 이터레이터를 반환 받아 순회하며 작업을 처리하면 된다.
즉 데이터 소비자들은 이터레이션 프로토콜만을 지원하면 되는 것이다.
사진 출처 - 이웅모 님 블로그
그럼 내장 이터러블이 아니면 이 좋은 API들을 못 쓰는가? 꼭 그렇지는 않다.
내가 Object에 for...of나 Spread연산자 등을 사용하고 싶다면 Object를 이터러블 객체로 바꿀 수 있다. 이터러블 프로토콜을 따르는 Object를 만들면 되는 것이다.
아래의 코드를 살펴보자.
const obj = {};
console.log(Symbol.iterator in obj);
[...obj];
보다시피 obj는 @@iterator를 프로퍼티로 갖고 있지 않다. 그래서 Spread연산자를 사용하고 싶어도 not iterable error를 받을 뿐이다.
그럼 obj의 프로퍼티에 @@iterator를 추가해보자.
const obj = {
[Symbol.iterator]() {
let a = 1;
return {
next() {
return a < 5 ? {value: a++, done: false} : {value: undefined, done: true};
}
}
}
};
console.log(Symbol.iterator in obj);
const iterator = obj[Symbol.iterator]();
iterator.next();
iterator.next();
iterator.next();
iterator.next();
iterator.next();
obj가 객체임에도 불구하고 이터러블 객체와 유사한 동작을 보여주고 있다. 그렇다면 과연 obj는 for...of문이나 Spread연산자도 사용할 수 있을까?
const obj = {
[Symbol.iterator]() {
let a = 1;
return {
next() {
return a < 5 ? {value: a++, done: false} : {value: undefined, done: true};
}
}
}
};
const iterator = obj[Symbol.iterator]();
for(item of obj) {
console.log(item);
};
[...obj];
놀랍게도! 된다. 이로써 obj는 커스텀 이터러블 객체가 되었다.
이제 이터레이터의 내부가 어떻게 되어있는지 조금이나마 감이 잡히는 듯한 안 잡히는 듯한... 기분이다.
처음 이 개념을 접했을 때, 사실 나는 일부러 개념을 익히기를 피했던 것 같다. 메소드와 프로퍼티가 무슨 차이인지, 이 함수는 무엇을 하며 무엇을 반환하는지, 이건 왜 이렇지? 여긴 왜 소괄호가 붙어있고 여긴 왜 중괄호고 여긴 왜 대괄호고... 이런 느낌이었기 때문에 교재의 코드를 실행시키기에 급급했다.
이제는 보이기 시작했다. 아무 것도 모르고 입문한지 1년이 다 되가는 타이밍에서야 코드가 보인다는 것은 성장 속도가 더딘 게 아닌가 싶기도 하다.
한 편으로는 '자신이 다 안다는 생각이 들면 아무 것도 모른다는 뜻이다.' 라는 말도 떠오른다. 나는 어떤 개념을 익혔다고 생각하면 항상 이 문구를 되새긴다. 정말로 그랬기 때문이다.
아무튼, 다음은 Promise.all 메소드와 generator쪽으로 알아볼 생각이다.