All Articles

NodeList 다루기 (이터러블/이터레이터 프로토콜)

NodesList란 무엇인가요?

queryselectorAll로 html element를 선택해본 분들은 Nodelist라는 것을 잘 아실 겁니다.

<body>
  <div class="container">1</div>
  <div class="container">2</div>
  <div class="container">3</div>
  <div class="container">4</div>
</body>

<script>
  const containers = document.querySelectorAll(".container");
  console.log("containers ", containers);

  // containers
  // NodeList(4) [div.container, div.container, div.container, div.container]
</script>

꼭 생긴건 배열같이 생겼지만, 이걸 그대로 filter같은 배열 메소드를 사용하려고 하면 오류가 납니다.

<script>
  const number1 = containers.filter((container) => container.innerText === "1");
  //Uncaught TypeError: containers.filter is not a function
</script>

이제 이걸 사용하고 싶다면 아래와 같이 바꾸어 주어야 합니다.

//1. Array.prototye.call을 사용하는 방법
const number1 = Array.prototype.filter.call(
  containers,
  (container) => container.innerText === "1"
);

//2. 배열로 전개연산하여 사용
const number1 = [...containers].filter(
  (container) => container.innerText === "1"
);

아무튼 궁금한 점이 생기게 됩니다. 도대체 Nodelist는 뭐길래 이런 속성을 가지고 있는 것일까?



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

ES6에서는 Symbol이라는 개념이 추가되었고 기존 리스트 순회와는 다른 점들이 발생합니다.

Symbol이라는 개념을 완벽히 이해하기 위해선 한 포스팅을 전부 할애해야 하므로 여기서는 단순하게 string, boolean과 같은 원시타입이라고 생각하시면 되는데요.

이 Symbol이 바로 오늘 이야기할 이터러블/이터레이터 프로토콜을 가능하게 만드는 개념입니다.

Nodelist의 프로토타입 체이닝을 살펴보면 아래와 같은 Symbol.iterator를 발견할 수 있습니다.


이 iterator는 {value, done} 객체를 리턴하는 값을 가지고 있는 특성이 있습니다.

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

done이 true가 나올때까지 순회하면서 값을 반환하게 됩니다. 이 이터레이터 프로토콜은 Map, Set과 같은 곳에서도 사용되는 것을 볼 수 있습니다.

이터레이터를 만드는 제너레이터 함수

function* range(start, stop) {
  for (var i = start; i < stop; i += 1) {
    yield i;
  }
}

generator를 사용해서 이터레이터 프로토콜을 따르는 객체를 만들 수 있습니다. 앞에다가 *를 붙이고 yield 키워드를 생성해서 만들면 됩니다. 이렇게 작성하면 쉽게 사용자 정의 함수형 프로그래밍을 사용할 수 있습니다.

그렇다면 왜 사용하는 걸까? 장점은?

장점

이렇게 ES6식으로 리스트 순회가 바뀐 것은 알겠는데 어떤 장점이 있길래 바뀐 걸까요?

위의 블로그를 참고해서 글을 써보면 Symbol.iterator의 사용방법 및 장점이 여러가지가 나와있는데요. 참고해 보시면 좋을 것 같습니다. 그 중 가장 직접적으로 이해되는 부분은 작동방식이 다름에 따라 효율성을 볼 수 있다는 것인데요.

예를 들어 Infinity를 직접 다루면 상당히 위험합니다. 싱글스레드인 javascript 언어 특성상 스택에서 freezing이 되어 버리는 경우가 있습니다.

하지만 제너레이터, 이터러블 프로토콜을 통해 만든 객체는 다른식으로 작동을 하게 되므로 무한수열(Infinity)를 다룰 수 있습니다.

function* range(start, stop) {
  for (var i = start; i < stop; i += 1) {
    yield i;
  }
}

const myIter = range(5, Infinity)  //무한수열을 다룰 수 있음

console.log(myIter.next()) 5
console.log(myIter.next()) 6

크롬 디버거로 작동방식을 살펴보게 되면 range 함수를 호출함에도 콜스택에 쌓이지 않고 바로 넘겨가는 것을 볼 수 있습니다. 이터러블 제너레이터 프로토콜을 통해 만든 함수는 필요한만큼만 꺼내서 만드는 방식으로 되어 있습니다.

따라서 함수형으로 map, filter 등을 중첩하여 사용한다고 하더라도 큰 객체 (예를들어 대용량의 JSON 파일)을 다루더라도 매우 효율적으로 사용할 수 있습니다.