순회와 이터러블 : 이터레이터 프로토콜

이효범·2022년 4월 25일
0
post-thumbnail

리스트 순회

함수형 프로그래밍에서 리스트 순회는 굉장히 중요하며 실무적인 프로그래밍에서도 리스트 순회는 굉장히 중요하다. 자바스크립트가 ES6가 되면서 리스트 순회가 많이 달라진 점이 있다. 그리고 이는 언어적으로 자바스크립트가 큰 발전을 이뤄낸 부분이기 하다.

어떻게 이러한 규약이 만들어 졌고, 어떻게 리스트 순회가 이루어 지는지 상세하게 알아보도록 하자.

## 기존과 달라진 ES6에서의 리스트 순회
  - for i++
  - for of
  
  // ES6 이전
  const list = [1, 2, 3];
  for (var i = 0; i < list.length; i++) {
   console.log(list[i]); 
  };

  const str = 'abc';
  for (var i = 0; i < str.length; i++) {
   console.log(str[i]); 
  };

  // ES6 
  // 보다 간결해졌고, 보다 선언적으로 리스트를 순회한다.
  for (const a of list) {
   console.log(a); 
  }

  for (const a of str) {
   console.log(a); 
  }

Array, Set, Map을 통한 이터러블, 이터러블/이터레이터 프로토콜

Array, Set, Map은 자바스크립트의 내장 객체로써 iterable/iterator protocol 를 따르고 있다.

자바스크립트에서 이터러블, 이터러블/이터레이터 프로토콜은 굉장히 중요하다. 이터러블 프로토콜을 정확히 익히고, 이터러블의 추상을 정확히 다루면 자바스크립트에서 보다 이 값들을 잘 사용한 함수들을 만들고 값들을 잘 다룰 수 있다.

Array

### Array => iterable
const arr = [1, 2, 3];
for (const a of arr) console.log(a); // 1 2 3

console.log(arr[0]); // 1

console.log(arr[Symbol.iterator]); // f values() {[native code]}
// 만약 arr[Symbol.iterator] = null; 을 적용시키면? array 또한 not iterable 해진다. 

console.log(arr[Symbol.iterator]()); // Array Iterator {}
let iterator = arr[Symbol.iterator]();
iterator.next();  // {value: 1, done: false}
iterator.next();  // {value: 2, done: false}
iterator.next();  // {value: 3, done: false}
iterator.next();  // {value: undefined, done: true}

// Array는 iterable 이고, Symbol.iterator 를 통해서 이터레이터를 리턴하기 때문에 
// for of 문과 함께 잘 동작하는 iterable 객체이고, 따라서  iterable/iterator protocol 를 따른다고 말할 수 있다.

// 다른 예시
const arr = [1, 2, 3];
let iter1 = arr[Symbol.iterator]();
iter1.next(); // next를 했기 때문에 밑의 for...of 문에서는 두번만 순회한다.
for (const a of iter1) console.log(a);  // 2 3

// 정리해보자, 이 Array는 Symbol.iterator 를 실행한 이터레이터를 계속해서 순회하면서 안쪽의 value로 떨어지는 값을 출력하고 있는 것이다.

Set

### Set
const set = new Set([1, 2, 3]);
for (const a of set) console.log(a); // 1 2 3

console.log(set[0]); // undefined
// Set은 위처럼 숫자로 접근할 수 있는 값이 없음에도 불구하고 for...of 문이 동작하는 것은
// 증가하는 i값을 key-value 로 접근해서 순회하는 것이 아니라 
// iterator protocol 를 따르고 있기 때문에, 또 for...of 문 역시 이를 따르고 있기 때문에 
// 함께 동작하는 것이다.

let a = set[Symbol.iterator]();
a.next(); // { value: 1, done: false}; 
a.next(); // { value: 2, done: false}; 
a.next(); // { value: 3, done: false}; 
a.next(); // { value: undefined, done: true}; 

Map

### Map
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
for (const a of map) console.log(a); // ['a', 1] ['b', 2] ['c', 3]

console.log(map[0]); // undefined

let a = set[Symbol.iterator]();
a.next(); // { value: Array(2), done: false }; => { value: ["a", 1], done: false };
a.next(); // { value: Array(2), done: false }; => { value: ["b", 2], done: false };
a.next(); // { value: Array(2), done: false }; => { value: ["c", 3], done: false };
a.next(); // { value: undefined, done: true }; 

---
  
// 다른 예시
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
let iter1 = map[Symbol.iterator]();
iter1.next();  // next를 했기 때문에 밑의 for...of 문에서는 두번만 순회
for (const a of map) console.log(a); // ["b", 2] ["c", 3]

map.keys(); // MapIterator {"a", "b", "c"}
// keys 라는 함수는 Iterator를 리턴한다. 그리고 Iterator는 next를 했을 때 value의 키만 담게 된다.
let it = map.keys(); 
it.next(); // { value: "a", done: false }
it.next(); // { value: "b", done: false }
it.next(); // { value: "c", done: false }
// 이를 이용해 다음과 같은 방식도 사용할 수 있다.
for (const a of map.keys()) console.log(a); // a b c
for (const a of map.values()) console.log(a);  // 1 2 3
for (const a of map.entries()) console.log(a);  // ["a", 1] ["b", 2] ["c", 3]

// 여기서 map.values() 또한 Iterator를 리턴하는데, 여기서 다음과 같이 다시 Symbol.iterator를 확인해보면,
let it2 = map.values(); // MapIterator {1, 2, 3}
it2[Symbol.iterator]  // f [Symbol.iterator]() {[native code]}
// 이터레이터로 만든 것이 또 Symbol.iterator 를 가지고 있다. 
// 그래서 for...of 문에서 entries의 결과를 Symbol.iterator를 실행한 것을 가지고 다시 순회를 하는 것이다. 
// 즉, Symbol.iterator는 또다시 이터레이터를 리턴을 하는데, 결국 자기자신을 그대로 리턴하도록 되어 있다.

정리

iterable/iterator protocol

iterable : 이터레이터를 리턴하는 [Symbol.iterator] 메소드를 가진 값
iterator : { value, done } 객체를 리턴하는 next 메소드를 가진 값
iterable/iterator protocol : 이터러블을 for...of, 전개 연산자 등과 함께 동작하도록한 규약

참고

1. Set, Map
Set, Map에 대해서 필자의 개인블로그에 상세하게 정리해놓았다. 다음 글을 참고하자.
[Set과 Map]

2. 심볼
심볼은 문자열 대신 유니크한 key를 생성하기 위한 도구이며, 서로 다른 개발자들끼리 약속한 키를 만들때 유용하다.

javascript.info/symbol


사용자 정의 이터러블, 이터러블/이터레이터 프로토콜

지금까지는 자바스크립트 내장 객체인 Array, Set, Map이 for...of문으로 어떻게 순회되는지 살펴보았다.
즉, 내장 iterable이라고 할 수 있는 위 객체구조들이 이터러블/이터레이터 프로토콜을 따르고 있는 for...of문과 어떻게 함께 동작해서 순회가 되는지에 대해서 알아봤다.

이번에는 사용자 정의 이터러블을 구현해보며, 이터러블에 대해서 더 정확하게 알아보도록 하자.

const iterable = {
  [Symbol.iterator]() {
   let i = 3;
   return { // Iterator를 반환한다.
     next() { // next 메소드를 통해서 내부의 값을 순회할 수 있다.
      return i === 0 ? { done: true} : { value: i--, done: false }; 
     }
   }
  }
};
let iterator = iterable[Symbo.iterator]();
console.log(iterator.next());  // { value: 3, done: false }
console.log(iterator.next());  // { value: 2, done: false }
console.log(iterator.next());  // { value: 1, done: false }
console.log(iterator.next());  // { done: true }

for (const a of iterable) console.log(a); // 3 2 1 
// iterable에 [Symbol.iterator]가 구현되어 있기 때문에 for...of 문에 
// 들어갈 수 있다.


// 그런데 아직은 자바스크립트의 이터러블, 이터러블/이터레이터 프로토콜의 모든 속성을 구현하지는 못했다.
// 어떤 부분이 덜 구현이 되었냐면, 
const arr = [1, 2, 3];
let iter1 = arr[Symbol.iterator]();
iter1.next(); 
for (const a of iter1) console.log(a);  
// 위의 자바스크립트의 배열처럼, 잘 구현된 이터러블은 이터러블을 만들었을 때 
// 이터레이터를 진행하다가 순회를 할 수도 있고, 그냥 그대로 for...of 문에 넣었을 때도
// 그대로 모든 값들을 순회할 수 있어야 한다.
// 이는 즉, 위의 iter2 역시 [Symbol.iterator] 를 가지고 있다는 것
console.log([Symbol.iterator]); // f [Symbol.iterator]() {}
// 그리고 이 심볼 이터레이터를 실행한 값은 자기자신이다.
console.log([Symbol.iterator]() === iter1);  // true

// 결국, 이터레이터가 자기 자신을 반환하는 심볼 이터레이터 메소드를 가지고 있을 때
// 잘 구현된 이터러블이라고 할 수 있는 것이다.
// 그럼 우리가 직접 만든 이터러블을 리팩토링 해보도록 하자.

const iterable = {
  [Symbol.iterator]() {
   let i = 3;
   return { // Iterator를 반환한다.
     next() { // next 메소드를 통해서 내부의 값을 순회할 수 있다.
      return i === 0 ? { done: true} : { value: i--, done: false }; 
     },
     [Symbol.iterator]() { return this; } // 자기자신을 반복,
   } // 이전까지 진행되어있던 자기의 상태에서 계속해서 next를 할 수 있도록 해준다.
  }
};

let iterator = iterable[Symbol.iterator]();
for (const a of iterator) console.log(a); // 3 2 1
// 이제는 iterable를 순회를 해도 순회가 되고, iterator로 만든 상태에서
// 순회를 해도 순회가 잘 되는 것을 확인할 수 있다. 또한 일정 부분 진행한 
// 상태에서 순회를 해도 순회가 되고 있다. 



// 추가적인 이터러블, 이터러블/이터레이터 프로토콜 예시
for (const a of document.querySelectorAll('*')) console.log(a);
const all = document.querySelectorAll('*');
console.log(all[Symbol.iterator]);
console.log(all[Symbol.iterator]());
let iter3 = all[Symbol.iterator]();
console.log(iter3.next());
console.log(iter3.next());
console.log(iter3.next());

전개 연산자

전개 연산자도 마찬가지로 이터러블, 이터러블/이터레이터 프로토콜를 따른다.

const a = [1, 2];
console.log(...a); // 1 2
console.log([...a, ...[3, 4]]); // [1, 2, 3, 4]

const b = [1, 2];
b[Symbol.iterator] = null;
console.log([...b, ...[3, 4]]); // b is not iterable

console.log([...a, ...arr, ...set, ...map.values()]); 

// 전개 연산자 역시 이터러블 프로토콜을 따르고 있는 값들을 펼칠 수 있는 것이다.
profile
I'm on Wave, I'm on the Vibe.

0개의 댓글