TIL 12 | ES6 순회와 이터러블

grighth12·2021년 8월 11일
1

TIL

목록 보기
12/15
post-thumbnail

배열의 순회에 우리는 익숙하게 for문을 사용한다. ES 6에서는 for... of 문이 새롭게 등장했다. 둘은 뭐가 다를까?

ES6의 순회

ES5에서는 아래와 같이 for문에서 i를 증감하며 배열을 순회했다.

//ES5
const list = [1, 2, 3];
for (var i = 0; i < list.length; i++) {
  console.log(list[i]);
}
// 1 2 3
const str = "abc";
for (var i = 0; i < str.length; i++) {
  console.log(str[i]);
}
// a b c

ES6에서는 for... of 문이 등장하여 좀더 간결한 방식으로 순회할 수 있게 되었다.

const list = [1, 2, 3];
for (const a of list) {
  console.log(a);
}
// 1 2 3
const str = "abc";
for (const a of str) {
  console.log(a);
}
// a b c

ES6의 순회방식이 ES5의 순회방식보다 선언적이고 간결하게 만들어준다.

보다 선언적이라는 것은 인간이 하는 일이 줄었다는 의미로 봐도 무방하다. ES5에서는 변수를 선언하고, 증감하고, 조건을 달아 컴퓨터에게 하나하나 명령해주었다. ES6에서는 그런 과정을 컴퓨터에게 위임하고 있다. 또한 그런 구체적인 과정이 감춰졌기 때문에 ES5보다 ES6가 더 추상화 되어 있다고 말할 수 있다.

하지만 for...of에는 그 이상의 의미가 있다. for...of가 개발자에게 어떠한 규약을 열어주고, 어떻게 순회에 대해 추상화를 했고, 어떻게 사용하도록 했는지 알아보자.

Array, Set, Map

Array, Set, Map 객체는 모두 for...of 문으로 순회할 수 있다. 이 쯤에서for...of문은 for문 처럼 동작할까?하는 궁금증이 생긴다.

for(let i = 0 ; i < obj.length ; i++) === for (const o of obj) 일까?

그렇다면 Array 뿐만 아니라 Set, Map 객체도 인덱스로 접근이 가능해야 한다. 아래 함수의 실행 결과는 어떻게 될까?

const set = new Set();
set.set("a");
set.set("b");
console.log(set[1]) // "a"가 나올까?

실제로 확인해보면 위의 코드는 undefined를 출력한다.

for...of문과 for문의 동작 방식은 다르다는 것을 유추할 수있다.

결론적으로, for...of문은 대상이 이터러블/이터레이터 프로토콜을 따를 때 사용할 수 있다.

이터러블/이터레이터 프로토콜

자바스크립트의 Array, Map, Set은 이터러블/이터레이터 프로토콜을 따른다.

  • 이터러블 : 이터레이터를 리턴하는 [Symbol.iterator]()를 가진 값
    • ex ) Array, Set, Map
    • arr[Symbol.iterator]() 실행시 iterator를 리턴
  • 이터레이터 : { value, done } 객체를 리턴하는 next 함수를 가진 값
    • iterator.next() // return {value, done}
    • value : 값, done : 순회가 끝났는지 여부
  • 이터러블/이터레이터 프로토콜 : 이터러블을 for...of, 전개 연산자 등과 함께 동작하도록 한 규약

for...of의 동작을 정리하면 다음과 같다.

  • [Symbol.iterator]()를 실행하여 iterator를 리턴
  • iterator.next()를 실행하여 반환된 value를 출력하다가, done이 true가 되면 빠져나옴

Map 자료구조를 예로 알아보자.

const map = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3],
]);

// [Symbol.iterator]에 null을 대입하면 Uncaught TypeError: map is not iterable 에러가 난다.
// map[Symbol.iterator] // === f entries() {}
/*
  Array라면?
  arr[Symbol.iterator] // f values() {}
  Set이라면?
  set[Symbol.iterator] // f values() {}
*/
for (const a of map) { // a에는 value가 넘어옴 (value의 형태는 데이터 타입마다 다를 수 있음)
  console.log(a);
}

또한 이터레이터를 이용하여 진행지점을 기억하여 순회를 중단하고 재시작 할 수 있다. 아래 예제를 통해 arr의 이터레이터를 받아 순회를 진행하다가, 다시 순회를 진행할 경우 진행지점을 기억하고 있음을 알 수 있다.

const arr = [1, 2, 3];

const iter = arr[Symbol.iterator](); // 이터레이터

iter.next(); // {value : 1, done: false}
iter.next(); // {value : 2, done : false}
// 여기까지 실행되면, 다시 iter를 순회할 경우 처음부터가 아니라 다음 지점부터 순회한다

for (const a of iter) {
  console.log(a);
} // 3

Map의 경우 map.keys()는 키 값을 value로 하는 iterator를 반환한다. 이터레이터를 반환하는 map.keys(), map.values(), map.entires()for..of 문에 사용 가능하다. 그렇다는 것은, 이터레이터를 반환하는 세가지 함수들이 [Symbol.iterator]를 가지고 있는 이터러블이라는 것이다.

const it = map.keys(); // 이터러블
const iterator = it[Symbol.iterator](); // 이터레이터
iterator.next(); // {value : a, done: false}

사용자 정의 이터러블 알아보기

사용자 정의 이터러블은 [Symbol.iterator]()를 property로 가지게하면 구현할 수 있다. 아래 iterableSymbol.iterator()를 가지고 있기 때문에 이터러블이다.
,

const iterable = {
  [Symbol.iterator]() {
    let i = 3;
    return {
      next() {
        return i === 0 ? { done: true } : { value: i--, done: false };
      }
    };
  },
};

const iterator = iterable[Symbol.iterator]();
iterator.next(); // { value : 3, done : false}
iterator.next(); // { value : 2, done : false}

// 해당 이터레이터는 진행 지점을 기억하지 못함
// 아래 코드의 출력은 3, 2, 1이 된다.
for(const a of iterable) {
  console.log(a);
} 

주석을 달아 놓았듯이 아직까지는 Array와 같은 데이터 형식의 이터레이터와 달리 진행지점을 기억하지 못한다. 진행 지점을 기억하게 하기 위해서는 아래와 같이 수정해야 한다.

const iterable = {
  [Symbol.iterator]() {
    let i = 3;
    return {
      next() {
        return i === 0 ? { done: true } : { value: i--, done: false };
      }, 
      // 자기 자신을 리턴하는 [Symbol.iterator]() 추가
      [Symbol.iterator]() {
        return this; 
      },
    };
  },
};

const iterator = iterable[Symbol.iterator]();
iterator.next(); // { value : 3, done : false}
iterator.next(); // { value : 2, done : false}

// 아래 출력 결과는 1이 된다.
for(const a of iterable) {
  console.log(a);
} 

이처럼 이터레이터 안에 자신을 리턴하는 [Symbol.iterator]()가 있으면 진행 지점을 기록할 수 있고, 이를 well-formed iterator 라고 한다. well-formed iterator는 [Symbol.iterator]()에서 this를 반환함으로써 이전에 진행했던 자신의 상태에서 next를 진행 할 수 있도록 한다.

Array를 통해 확인해보면 실제로 그렇게 구현되어 있음을 확인할 수 있다.

const arr = [1, 2, 3];
let iter = arr2[Symbol.iterator]();
iter.next();

// iter 역시 자기자신을 반환하는 Symbol.iterator를 가지고 있음
console.log(iter[Symbol.iterator]() === iter); // true

또, well-formed iterator는 [Symbol.iterator]()를 가지므로 이터레이터이자 이터러블이라고 할 수 있다. 따라서 아래와 같은 코드가 가능해진다.

// iterator를 넣어도 되고 iterable을 넣어도 된다
// iterable도 iterator를 반환하고, iterator도 자기 자신(iterator)을 반환하기 때문
for (const a of iterable) console.log(a);
for (const a of iterator) console.log(a);

이터러블/이터레이터의 활용

단순히 위에서 살펴본 Array, Set, Map 같은 자바스크립트의 내장값들 뿐만 아니라, 많은 오픈소스, 라이브러리에서 순회가 가능한 형태의 값은 대부분 이터러블/이터레이터 프로토콜을 따르기 시작했다. 예를 들어 facebook의 Immuntable도 이를 따른다. 또한, Web APIs에 있는 많은 값들(ex. DOM 과 관련된 값들)도 이런 프로토콜들을 따르고 있다.

예를 들면 document.querySelctorAll은 배열처럼 보이는 NodeList 객체를 반환하는데 이는 ES5의 for문으로 순회할 수 없다. NodeList는 이터러블/이터레이터 프로토콜을 따르기 때문에 for...of문으로 순회할 수 있다.

for (const a of document.querySelectorAll("*")) {
  console.log(a);
}

const all = document.querySelectorAll("*");
console.log(all); // Array가 아닌 NodeList
console.log(all[Symbol.iterator]); // f values() {}
let iter = all[Symbol.iterator]();

console.log(iter.next()); 

전개 연산자

전개 연산자 역시 이터러블 프로토콜을 따르고 있다.

const a = [1, 2];
// 주석 해제할 경우 not iterable 에러 발생
// a[Symbol.iterator] = null;
console.log(...a); // 1, 2
console.log([...a, ...arr, ...set, ...map.values()]);

마치며

for... of 문을 잘 사용하고 있기는 했지만 내부 동작은 ES5의 for문과 크게 차이가 없을 줄 알았는데 많은 사실을 깨우칠 수 있었다. 전개 연산자 이외에도 Array.from과 같은 함수도 iterable을 전달 받아 동작한다고 한다. 생각보다 많은 부분에 퍼져있었다.

출처

프로그래머스 프론트엔드 데브코스 Day6 [강의] ES6 순회와 이터러블
profile
데굴데굴 굴러가고 있습니다

0개의 댓글