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

2024. 3. 10. 16:47함수형 프로그래밍

이터레이션 프로토콜

순회 가능한 데이터 컬렉션(자료구조)을 만들기 위해 ECMAScript 사양에 정의하여 미리 약속한 규칙이다.

이터레이션 프로토콜에는 이터러블 프로토콜이터레이터 프로토콜이 있다.

 

이터러블

이터러블 프로토콜을 준수한 객체를 이터러블이라고 한다.

이터러블 프로토콜은 Symbol.iterator 메서드를 호출하면 이터레이터 프로토콜을 준수한 이터레이터를 반환하는데,

이러한 규약을 이터러블 프로토콜이라고 한다. 그리고 이 규약을 지키는 객체가 이터러블이다.

포인트는 이터러블 객체의 Symbol.iterator 메서드를 호출할 때 반환되는 객체가 이터레이터이다.

const arr = [1, 2, 3];
console.log(Symbol.iterator in arr);

let iter1 = arr[Symbol.iterator]();
console.log(iter1); // 이터레이터를 반환

// 1. for ... of로 순회가 가능하다.
for (const item of arr) {
  console.log(item);
}
for (const a of iter1) {
  console.log(a);
}

// 2. 스프레드 문법의 대상이 된다.
console.log([...arr]);

// 3. 구조분해 할당
const [a, ...rest] = arr;
console.log(a, rest);

 

일반 객체는 Symbol.iterator 메서드를 직접 구현하지 않고, 상속받지도 않기 때문에 이터러블이 아니다.

단, 예외적으로 일반 객체의 스프레드 문법 사용은 허용된다.

const obj = { a: 1, b: 2, c: 3 };
console.log(Symbol.iterator in obj);

// ❌
for (const k of obj) {
  console.log(k);
}

// 단, 일반 객체에 스프레드 문법의 사용을 허용
console.log({...obj});

 

이터레이터

이터러블의 Symbol.iterator 메서드를 호출하면 이터레이터 프로토콜을 준수하는 이터레이터를 반환한다.

이터레이터는 next 메서드를 갖는다. next 메서드는 이터러블의 각 요소를 순회하기 위한 포인터 역할을 한다.

즉, next 메서드를 호출하면 한 단계씩 요소를 순회하며 순회 결과를 나타내는 이터레이터 리절트 객체를 반환한다.

const arr = [1, 2, 3];
let iter1 = arr[Symbol.iterator]();
console.log(iter1.next());
console.log(iter1.next());
console.log(iter1.next());
console.log(iter1.next());

const set = new Set([1, 2, 3]);
let iter2 = set[Symbol.iterator]();
console.log(iter2.next());
for (const a of iter2) {
  console.log(a); // 탐색한 요소를 제외하고 순회한다.
}
{ value: 1, done: false }

이렇게 순회 결과를 나타내는 이터레이터 리절트 객체를 반환하는 것을 알 수 있다.

 

맵이터러블 객체

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

Map 자료형의 대표적인 세 가지 메서드이다.

위 세가지 메서드를 사용해도 for ... of 순회가 가능하다는 것은 세가지 메서드가 반환하는 값이 이터러블 객체라는 것이다.

MDN에서도 새로운 순회 가능한 이터러블 객체를 반환한다고 명시되어있다.

 

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

console.log(Symbol.iterator in map.entries());
const iter1 = map.entries()[Symbol.iterator]();
console.log(iter1.next());
console.log(iter1.next());
console.log(iter1.next());
console.log(iter1.next());

그래서 다음과 같이 map.entries()가 반환하는 이터러블 객체에 Symbol.iterator를 호출해 iter1 이라는 이터레이터를 생성하고 next 메서드를 호출하면서 이터레이터 리절트 객체를 반환한다.

 

정리하면 다음과 같다.

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

 

이터러블 프로토콜을 준수하도록 구현만 한다면 이터러블이 가능하다는 말이다. 사용자 정의 이터러블이 그러하다.

 

사용자 정의 이터러블

이터러블은 이터레이터를 반환하고 이터레이터는 next 메서드를 통해 이터레이터 리절트 객체를 반환한다는 것을 알고 있으면

다음과 같이 사용자 정의 이터러블을 만들 수 있다.

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

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

 

하지만 이터레이터 객체를 for ... of로 순회하려하면 오류가 생긴다.

즉, 불완전한 이터러블이라는 것이다.

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

const iterator = iterable[Symbol.iterator]();
for (const a of iterator) {
  console.log(a); // TypeError: iterator is not iterable
}

 

Well-Form 이터러블을 만들기 위해서 먼저 Array의 이터레이터에 Symbol.iterator를 호출해보자.

const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator]();
console.log(iterator[Symbol.iterator]() === iterator);

즉, 이터레이터의 Symbol.iterator를 호출하면 호출한 자기 자신과 동일하다는 것을 알 수 있다.

그렇기 때문에 위에서 직접 정의한 이터러블을 다음과 같이 수정해야한다.

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

const iterator = iterable[Symbol.iterator]();
for (const a of iterator) {
  console.log(a);
}

this는 Symbol.iterator를 호출한 이터레이터에 바인딩이 되기 때문에 이터레이터 객체도 for ... of 구문으로 순회할 수 있다.

 

유사 배열 객체이면서 이터러블인 것들

유사 배열 객체는 배열처럼 인덱스로 요소에 접근이 가능하고 length 프로퍼티를 가진 객체를 의미한다.

기본적으로 유사 배열 객체는 Symbol.iterator 메서드가 없기 때문에 for ... of 구문을 사용할 수 없다.

const a = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
};

for (let i = 0; i < 3; i++) {
  console.log(a[i]);
}

// ❌
for (const x of a) {
  console.log(x);
}

 

 

단, arguments, NodeList, HTMLCollection 은 유사 배열 객체이면서 이터러블이다.

for (const a of document.querySelectorAll('*')) console.log(a);
const all = document.querySelectorAll('*'); // NodeList
let iter3 = all[Symbol.iterator](); // NodeList는 유사 배열 객체이면서 이터러블
console.log(iter3.next());
console.log(iter3.next());
console.log(iter3.next());

 

'함수형 프로그래밍' 카테고리의 다른 글

코드를 값으로 다루기  (0) 2024.03.19
map, filter, reduce  (0) 2024.03.11
제너레이터  (0) 2024.03.10
일급 함수와 고차 함수  (0) 2024.03.10