💬 들어가기 앞서
유인동 님의 함수형 자바스크립트 강의를 기반으로 학습한 내용을 정리합니다. 관련해 더 탐구한 내용이나, 지엽적인 부분은 모던 자바스크립트 딥다이브와 You Don't Know JS Yet, ECMAScript Specification을 참고하여 검증, 정리하고 있습니다. 본문 내 잘못된 부분이나 궁금하신 부분은 언제든 코멘트 남겨주세요. 감사합니다.
📚 이터레이터 프로토콜
1️⃣ 기존(ES6 이전)의 리스트 순회
const list = [1, 2, 3];
for (var i = 0; i < list.length; i++) {
log(list[i]);
}
길이(length)라는 프로퍼티에 의존해서, 숫자라는 키로 내부의 값을 순회한다. 친근한 방식이다.
유사배열 역시 동일한 매커니즘으로 처리된다.
const str = 'abc';
for (var i = 0; i < str.length; i++) {
log(str[i]);
}
2️⃣ ES6 이후의 순회
for (const a of list) {
log(a);
}
for (const a of str) {
log(a);
}
for..of 반복문이 등장하였다. 어떻게 순회하는지의 과정을 명령적으로 기술하기보단 보다 선언적으로, 간결한 방식으로 변모한 것을 확인할 수 있었다.
const set = new Set([1, 2, 3]);
for (const a of set) console.log(a);
// 1
// 2
// 3
for..of 문을 이용해 Map이나 Set 구조를 돌려보면, 여타 객체와 마찬가지로 0번째 인덱스부터 하나씩 순회하는 것처럼 보인다.
그렇다면 for..of는 내부적으로 기존의 순회 방식(숫자 기반의 인덱싱)과 같이 동작하는 것일까?
set[0], map[0] 을 찍어보면, 의도하는 값이 나오지 않는 것을 확인할 수 있다. 따라서, for..of는 숫자 키의 형태로 객체를 순회하는 방식과 같이 동작하지 않는다. 정도의 가설을 세울 수 있을 것이다. 여기서 등장하는 개념이 이터러블(Iterable)과 이터레이터(Iterator)이다.
3️⃣ Symbol.iterator
배열을 포함한 Map, Set 등의 자료 구조들은 [Symbol.iterator]를 갖고 있다. 설명하기 앞서 선언해놨던 변수 arr의 이터레이터 심볼을 null 로 설정한 후, for..of를 통해 순회해보자.
const arr = [1, 2, 3];
arr[Symbol.iterator] = null;
for (const a of arr) console.log(a);
에러가 발생하며 순회가 되지 않는다. Map도, Set도 동일한 결과를 보인다.
Symbol.iterator와 for..of문은 어떤 연관관계가 있을까?
4️⃣ 이터러블 / 이터레이터 프로토콜
각 용어에 대한 정의는 아래와 같다.
- 이터러블: 이터레이터를 리턴하는 심볼 [Symbol.iterator]()를 가진 값
- 이터레이터: { value, done } 객체를 리턴하는 next()를 가진 값
- 이터러블 / 이터레이터 프로토콜: 이터러블을 for..of, 전개 연산자 등과 함께 동작하도록 정한 규약
Array는 이터러블하다. 즉 이터레이터를 리턴하는 [Symbol.iterator]()를 갖고 있다. 그래서 해당 값을 null로 부여했을 때, '해당 배열이 이터러블하지 않다'라는 TypeError가 발생한 것이다.
직접 콘솔에 찍어보면 직관적으로 확인할 수 있다.
이터러블한 Array 변수의 심볼을 호출하였더니 이터레이터를 반환하였고,
이터레이터의 next 메서드를 연속적으로 호출하니 value와 done 키를 갖는 객체를 반환하는 것을 확인할 수 있었다.
이 때 next()를 한 번 호출할 때마다 value가 다음 원소로 이동하고, 마지막 원소 위치에서 호출할 경우 undefined value와 Done 플래그까지 반환하는 것 또한 확인할 수 있었다.
따라서 Array, Map, Set과 같은 구조들은 [Symbol.iterator]()를 갖고 있고, 해당 심볼에서 반환하는 이터레이터의 next() 메서드를 통한 순회가 가능하기 때문에 이터러블 / 이터레이터 프로토콜을 따른다고 볼 수 있다.
다시 돌아와 for..of 문을 살펴보자. 이터러블한 요소들에 대해 next() 메서드가 호출되어 value를 하나씩 반환하고, done의 값이 true가 되는 시점에 루프를 빠져나오도록 짜여져 있기 때문에, 숫자를 통해 인덱싱할 수 없는 구조들에 대한 순회가 가능한 것이다.
5️⃣ ECMAScript Specific
ECMA Specification - ForIn/OfBodyEvaluation을 통해 검증해보았다.
유효성 검사 과정을 거친 후 iterator의 next() 메서드를 호출, done이 true일 경우 value를 반환한 후 해당 과정을 반복하고 있는 것을 확인할 수 있었다.
6️⃣ 이터러블 구현하기
직접 정의한 이터러블 객체를 for..of 문을 통해 순회시켜보자. 3, 2, 1 의 결과값을 출력시키고자 한다.
const iterable = {
[Symbol.iterator]() {
let i = 3;
return {
next() {
return i == 0 ? { done: true } : { value: i--, done: false };
},
};
},
};
잘 작동하는 것처럼 보인다. 그러나 기존의 이터러블과 다른 점이 있다면,
이터레이터 심볼을 재귀적으로 반환하고, 또한 이를 실행했을 때 자기 자신을 반환함으로써 어디에서든 이터레이터로 호출되었을 때 이전에 자신이 갖고 있던 상태값을 계속해서 순회할 수 있는 안전 장치가 존재한다는 것이다. 이를 Well-formed Iterator라고 부른다.
const iterable = {
[Symbol.iterator]() {
let i = 3;
return {
next() {
return i == 0 ? { done: true } : { value: i--, done: false };
},
[Symbol.iterator]() {
return this;
},
};
},
};
심볼을 호출할 수 있고, 갱신된 상태를 갖고 있는 이터러블을 반환하도록 로직을 추가해주었다.
7️⃣ 신기한 사실!
ES6의 구조 뿐만 아니라, 전후로 나온 오픈소스들, 브라우저에서 지원하는 웹 API까지 이터러블을 지원하고 있다. 이들이 순회 가능한 배열이라서가 아닌, 이터레이터 심볼을 갖고 있는 이터러블이 구현되어 있는 요소들이기 때문인 것이다.
for (const a of document.querySelectorAll('*')) console.log(a);
DOM 조작 메서드인 querySelectorAll의 반환값도 이터러블한 것을 확인할 수 있었다.
8️⃣ Deeper
- 기존의 index 기반 순회 방식이 있었음에도 불구하고, Iterator 개념을 도입한 이유가 뭘까?
- for in, forEach 등의 다른 순회 메서드도 이터러블과 연관이 있을까?
- 비동기 이터러블(for..of..await)는 어떻게 동작하는가?