가장 핵심적인 변화가 많이 일어난 ES6(ECMAScript 2015)에서는 전보다 리스트 순회 방법이 좋아졌는데, 이에 대해 공부했다.
예전의 리스트 순회 방법은 다음과 같았다.
var list = [1, 2, 3];
for (var i = 0; i < list.length; i++) {
console.log(list[i]);
}
다음으로 훨씬 간결해진 ES6+에서의 리스트 순회 방법을 보자.
var list = [1, 2, 3];
for (const a of list) {
console.log(a);
}
새로운 리스트 순회 for...of문에서 확인해 볼 포인트는
예전에는 리스트를 순회할 때, 어떤걸 순회하느냐에(ex) array, set, map 등) 따라 방법이 달랐는데, ES6+에서는 각 규약이 하나로 통합되면서 일관성이 유지된다는 것이다.
Array, Set, Map 세가지 통해 확인해보자.
<!DOCTYPE html>
<html>
<body>
<script>
console.log("-----Array-----");
const arr = [1, 2, 3];
console.log(arr);
for (const a of arr) {
console.log(a);
}
console.log("-----Array-----");
console.log("-----Set-----");
const set = new Set([1, 2, 3]);
console.log(set);
for (const a of set) {
console.log(a);
}
console.log("-----Set-----");
console.log("-----Map-----");
const map = new Map([
["a", 1],
["b", 2],
["c", 3],
]);
console.log(map);
for (const a of map) {
console.log(a);
}
console.log("-----Map-----");
</script>
</body>
</html>
이처럼 모두 for...of...문에 의해 순회됨을 확인할 수 있다.
이터러블(Iterable), 이터레이터(Iterator), Iterable/Iterator protocol(규약)
위 예시로 이터러블(Iterable), 이터레이터(Iterator), Iterable/Iterator protocol를 이해해보자.
위의 리스트 순회는 Symbol.iterator에 의해 수행된다.
이처럼 어떤 값이 Symbol.iterator를 가질 때 Iterable이라고 한다.
위에서 선언한 set은 Iterable이다.
Iterable인 이유는 위 사진을 보면 알 수 있듯이 set이 Symbol.iterator라는 메서드를 가지고 있기 때문이다.
Iterator이란 다음과 같이 { value, done } 객체를 return하는 next()를 가진 값을 말한다.
마지막 줄에서 알 수 있듯이 iterator는 iterator내부에서 더이상 순회할 값이 없을 때 done을 true로 줘서 외부에서 알 수 있게 한다.
결록적으로 JavaScript는 Symbol.iterator라는 규약을 통해 for...of를 동작 시키고 있음을 확인할 수 있다.
이 글의 초반부에 말했듯이 ES6에서는 Array, Set, Map 등은 서로 전혀 다른 객체이지만 하나의 규약을 통해 리스트를 순회한다.
마지막으로 사용자 정의 iterable로 이해해보자.
<!DOCTYPE html>
<html>
<body>
<script>
const iterable = {
[Symbol.iterator]: function () {
var limit = 3;
return {
next() {
return limit < 1
? { done: true }
: { value: limit--, done: false };
},
[Symbol.iterator]: function () {
return this;
},
};
},
};
const iter = iterable[Symbol.iterator]();
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
for (const i of iterable) console.log(i);
</script>
</body>
</html>
limit이 1보다 작을 때 done이 true가 되고, 1보다 크거나 같을 때 limit이 1씩 감소하며 done이 false가 된다.
만약 14라인처럼 자기 자신을 return하는 Symbol.iterator가 구현되어있지 않은 경우,
iterator가 한번 끝까지 돈 후 for...of...문을 통해 출력하고 싶은 경우 'is not iterable' 오류가 발생한다.
따라서 iterator는 반드시 자기 자신을 return하는 Symbol.iteraor가 구현되어있어야하고, 이를 well-formed iterator라고 한다.
이렇게 사용자 정의 iterator를 사용하면 어느 시점까지 iterator가 진행이 된 상태에서 다시 진행이 가능하다.
이 경우를 보여주는 예시는 다음과 같다.
<!DOCTYPE html>
<html>
<body>
<script>
const arr = [1, 2, 3];
const iter2 = arr[Symbol.iterator]();
console.log(iter2.next());
for (const i of iter2) console.log(i);
</script>
</body>
</html>
실행 결과를 보면 알 수 있듯이 next()를 통해 iterator가 한번 진행 된 후, for...of 문을 통해 그 다음 2부터 출력되고 있다.
결국, 이 iterator개념들은 어떻게 활용되는가?
웹브라우저에서 웹 API들 중 순회가 필요한 값들은 이 iterable 프로토콜을 따르고 있다.
다음 간단한 예시로 이를 확인해 보자.
<!DOCTYPE html>
<html>
<body>
<script>
console.log(document.querySelectorAll("*"));
</script>
</body>
</html>
마찬가지로 Symbol.iterator가 구현되어있는 것을 확인할 수 있는데, Symbol.iterator가 구현되어있다는 뜻은 뭐냐? 이 역시 for...of문으로 가져올 수 있다는 뜻.
<!DOCTYPE html>
<html>
<body>
<script>
for (const a of document.querySelectorAll("*")) console.log(a);
</script>
</body>
</html>
iterable/iterator 프로토콜은 for...of뿐만 아니라 전개 연산자(Spread Operator)와도 함께 동작한다.
<!DOCTYPE html>
<html>
<body>
<script>
const list = [1, 2];
//list[Symbol.iterator] = null;
const set = new Set([1, 2, 3]);
const map = new Map([
["a", 1],
["b", 2],
]);
console.log([...list, ...set, ...map]);
</script>
</body>
</html>
만약 Symbol.iterator를 제거하면 'is not iterable' 오류가 생성된다.
이처럼 iterable/iterator 프로토콜은 여러 자바스트립트의 문법과 함께 사용되고 있는데, 이에 대해 다음 글에서 더 깊이 살펴보자.