오늘은 자바스크립트 ES6부터 바뀐 이터러블 객체의 순회 방법과 iterables, iterator에 대하여 공부했으나, 솔직히 다른나라 말 같기도 하고 워낙 생소한 개념이다 보니 잘 이해가 가지 않아 따로 공부할 겸 한번 정리해 보고자 한다.
var arr = [1, 2, 3, 4, 5];
for (var i = 0; i < arr.length; i++) {
console.log(arr[i]); // 1, 2, 3, 4, 5
}
ES6 이전의 문법에서는 오직 for문으로만 순회 가능한 객체들을 순회할 수 있었다. 이는 오직 순회 가능한 유사 배열객체의 length
프로퍼티 값에만 의존하여 객체를 순회했다는 뜻이다.
ES6에서는 for...of
문법으로 객체를 순회할 수 있다.
const arr = [1, 2, 3, 4, 5];
for (const value of arr) {
console.log(value); // 1, 2, 3, 4, 5
}
이것은 ES6에서 도입된 iteration protocol
때문이다. iteration protocol
은 데이터 컬렉션을 순회하기 위한 미리 약속된 규칙이다.
iteration protocol은 iterable protocol과 iterator protocol로 이루어져있다.
iterable
: iterator
를 반환하는 [Symbol.iterator]()
을 가진 값iterator
: {value, done}
을 반환하는 next()
를 가진 값iterable
은 순회 가능한 자료 구조를 뜻하고, iterator
는 순회 가능한 자료구조에서 요소를 탐색하기 위한 포인터 역할을 한다.iterable protocol
을 준수한 객체를 말한다.
const arr = [1, 2, 3];
console.log(arr[Symbol.iterator]) // [Function: values] iterator 반환
콘솔에 arr[Symbol.iterator]()
을 찍어보면, iterable 함수인 values
를 내장하고 있음을 알 수 있다. 따라서 배열은 iterable protocol
을 준수한 객체이다.
같은 방식으로 Set
, Map
객체를 만들고, [Symbol.iterator]()
를 콘솔에 찍어보면, 비슷한 iterator을 반환하는 것을 알 수 있다.
const set = new Set([1, 2, 3]);
console.log(set[Symbol.iterator]); // [Function: values]
const map = new Map([
["a", 1],
["b", 2],
["c", 3],
]);
console.log(map[Symbol.iterator]); // [Function: entries]
모든 iterable 프로토콜을 준수한 객체들은 for...of
를 이용하여 객체의 순환이 가능하다.
우리가 일반적으로 만드는 객체는 Symbol.iterator
메소드를 포함하지 않으므로, for...of
방식으로 순회가 불가능하다. (for ...in
방식으로는 순회 가능)
const obj = {a: 1, b: 2};
for (const char of obj) {
console.log(char) // Type 'Obj<number>' must have a '[Symbol.iterator]()' method that returns an iterator.
}
for (const char in obj) {
console.log(char) // a, b
}
iterable
을 준수한 객체의 [Symbol.iterator]()
가 반환하는 { value, done }
프로퍼티를 갖는 객체를 반환하는 next()
메서드를 갖는 객체이다. 이게 무슨 뜻인지 잘 이해가 안간다.
어떤 객체가 Iterable Protocol을 준수한다면,
1. 이 객체는 iterable[Symbol.iterator]
메서드를 소유한다.
2. iterable[Symbol.iterator]
메서드를 호출하면, iterator
가 반환된다.
3. 만약 이 객체가 이터레이터 프로토콜을 준수한다면, 반환된 iterator
는 next
메서드를 갖는다.
4. 반환된 iterator
의 next
메서드는, { value, done }
프로퍼티를 갖는 객체를 반환한다. 이를 iterator result
라고 한다. 이 객체는 순회 가능한 객체에서 포인터 역할을 한다.
const arr = [1, 2, 3];
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 }
이제 새로생긴 규약이 움직이는 원리에 대해 조금은 이해할 것 같다. 이제 for...of
문에 대하여 알아보자.
const arr = [1, 2, 3];
for (const el of arr) {
console.log(el); // 1, 2, 3
}
먼저 어떻게 동작하는지 추측하자면,
1. 배열은 Iteration Protocol을 준수하는 객체이다. 따라서 내부적으로 [Symbol.iterator]
메서드가 구성되어 있다.
2. Symbol.iterator
로 호출된 iterator
는 내부적으로 next
메서드를 갖는다.
3. next
메서드를 호출하면 iterator result
가 반환되고, 여기에 실질적인 값이 담겨있다.
따라서 for...of
문을 순회할 때, 내부적으로 next
를 호출하고, 모든 값이 호출되었다면, done: true
를 반환하여 순회를 종료한다고 생각된다.
"..."
react처럼 불변성 객체를 지향할 때도 그렇고, 우리는 꽤 자주 배열의 값 또는 객체의 값을 복사하여 사용한다. 그런데 Spread operator 또한 iteration protocol을 준수한 객체들에서만 사용할 수 있다고 한다. 객체 리터럴에 대한 Spread operator 규격이 ECMAScript2018에서 Object Initializer에 의해 정의되었다.
한번 몇가지 예시를 살펴보자.
const arr = [1, 2, 3, 4, 5];
const arr2 = [11, 12, 13, 14, 15];
const arr3 = [...arr, 6, 7, 8, 9, 10, ...arr2];
console.log(arr3); // [1, 2, 3, 4, 5, ..., 15]
다양한 용도로 사용될 수 있기 때문에 매우 강력한 도구가 될 수 있다.
// 이런 경우도 가능하다.
const r = [1, 2, 3, 4, 5]
function(...rest) {
console.log(...rest) // 1, 2, 3, 4, 5
}
단, spread operator로 복사하여 만들어진 새로운 객체는 Shallow copy이다. 따라서 1차원 배열보다 더 차수가 큰 배열일 경우, 내부 배열들은 참조 형태로 복사되므로 이에 주의하여야 한다.
객체도 spread operator를 이용하여 복사, 두 객체를 병합할 수 있다.
const obj1 = { a: 1, b: 2 }
const obj2 = { c: 3, d: 4 }
console.log({ ...obj1, ...obj2 }) // { a: 1, b: 2, c: 3, d: 4 }
근데 객체는 얘는 안된다.
const obj1 = { a: 1, b: 2 }
console.log(...obj1) // Type 'Obj<number>' must have a '[Symbol.iterator]()' method that returns an iterator.
우리가 만든 커스텀 객체는 [Symbol.iterator]
가 정의되어 있지 않다. console.log
에 마우스 커서를 올려보면, (Method) Console.log(...data: any[])
라고 매개변수에 들어갈 타입을 알려준다. any[]
배열처럼 [Symbol.iterator]
가 정의되어 있지 않기 때문인 걸로 보인다.
오늘은 ES6에 새롭게 정의된 Iteration Protocol에 대하여 정리해보았다. 처음 이 내용을 들었을 때에는 정말 외계어인줄 알았다. 그냥 배열이니까 iterable하고 순회가 가능한 것이 아닐까? 싶었는데, 자바스크립트의 역사를 살펴보니 이것이 엄청난 발전임을 깨달았다. 솔직히 파이썬에서는 당연히 지원하던 기능이었기 때문에 별 생각 없이 썼던게 크다. 🤪
이번 자바스크립트의 Iterator에 대해 공부하면서, 자바스크립트의 이터러블 객체 순회 원리에 대하여 더 잘 파악할 수 있었고, 저번에 LinkedList 만들면서 head
값이 null
이라 자꾸 순회 에러가 나던 이유를 이제는 알게 되었다.😃
https://poiemaweb.com/es6-iteration-for-of
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Spread_syntax