TIL9.자바스크립트의 이터러블 객체 순회 방법 (feat.ES6의 iterable과 iterator)

imloopy·2022년 3월 29일
0

Today I Learned

목록 보기
9/56

Today I Learned

오늘은 자바스크립트 ES6부터 바뀐 이터러블 객체의 순회 방법과 iterables, iterator에 대하여 공부했으나, 솔직히 다른나라 말 같기도 하고 워낙 생소한 개념이다 보니 잘 이해가 가지 않아 따로 공부할 겸 한번 정리해 보고자 한다.

배열, 객체, Map, Set 등 순회 가능한 객체의 순회 방법

ES5

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

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은 데이터 컬렉션을 순회하기 위한 미리 약속된 규칙이다.

Iterable/Iterator 프로토콜

iteration protocol은 iterable protocol과 iterator protocol로 이루어져있다.

  • iterable: iterator를 반환하는 [Symbol.iterator]()을 가진 값
  • iterator: {value, done}을 반환하는 next()를 가진 값
    즉, iterable은 순회 가능한 자료 구조를 뜻하고, iterator는 순회 가능한 자료구조에서 요소를 탐색하기 위한 포인터 역할을 한다.

iterable

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
}

iterator

iterable을 준수한 객체의 [Symbol.iterator]()가 반환하는 { value, done } 프로퍼티를 갖는 객체를 반환하는 next() 메서드를 갖는 객체이다. 이게 무슨 뜻인지 잘 이해가 안간다.

어떤 객체가 Iterable Protocol을 준수한다면,
1. 이 객체는 iterable[Symbol.iterator] 메서드를 소유한다.
2. iterable[Symbol.iterator]메서드를 호출하면, iterator가 반환된다.
3. 만약 이 객체가 이터레이터 프로토콜을 준수한다면, 반환된 iteratornext메서드를 갖는다.
4. 반환된 iteratornext메서드는, { 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문

이제 새로생긴 규약이 움직이는 원리에 대해 조금은 이해할 것 같다. 이제 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를 반환하여 순회를 종료한다고 생각된다.

전개 연산자(Spread operator) "..."

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

0개의 댓글