한국어

자바스크립트 이터레이터 프로토콜의 이해와 구현에 대한 포괄적인 가이드로, 향상된 데이터 처리를 위한 커스텀 이터레이터를 만드는 방법을 알려드립니다.

자바스크립트 이터레이터 프로토콜과 커스텀 이터레이터 심층 분석

자바스크립트의 이터레이터 프로토콜은 자료 구조를 순회하는 표준화된 방법을 제공합니다. 이 프로토콜을 이해하면 개발자는 배열이나 문자열과 같은 내장 이터러블을 효율적으로 다룰 수 있으며, 특정 자료 구조와 애플리케이션 요구사항에 맞는 자신만의 커스텀 이터러블을 만들 수 있습니다. 이 가이드는 이터레이터 프로토콜과 커스텀 이터레이터 구현 방법에 대한 포괄적인 탐구를 제공합니다.

이터레이터 프로토콜이란 무엇인가?

이터레이터 프로토콜은 객체를 어떻게 순회할 수 있는지, 즉 그 요소에 순차적으로 어떻게 접근할 수 있는지를 정의합니다. 이는 이터러블(Iterable) 프로토콜과 이터레이터(Iterator) 프로토콜, 두 부분으로 구성됩니다.

이터러블 프로토콜

객체는 Symbol.iterator 키를 가진 메서드가 있을 때 이터러블(Iterable)하다고 간주됩니다. 이 메서드는 이터레이터 프로토콜을 준수하는 객체를 반환해야 합니다.

본질적으로, 이터러블 객체는 자신을 위한 이터레이터를 생성하는 방법을 알고 있습니다.

이터레이터 프로토콜

이터레이터 프로토콜은 시퀀스에서 값을 검색하는 방법을 정의합니다. 객체는 두 개의 속성을 가진 객체를 반환하는 next() 메서드를 가질 때 이터레이터로 간주됩니다:

next() 메서드는 이터레이터 프로토콜의 핵심입니다. next()를 호출할 때마다 이터레이터는 다음 단계로 나아가고 시퀀스의 다음 값을 반환합니다. 모든 값이 반환되면 next()donetrue로 설정된 객체를 반환합니다.

내장 이터러블

자바스크립트는 기본적으로 이터러블한 몇 가지 내장 자료 구조를 제공합니다. 여기에는 다음이 포함됩니다:

이러한 이터러블은 for...of 루프, 전개 구문(...), 그리고 이터레이터 프로토콜에 의존하는 다른 구문들과 직접 사용될 수 있습니다.

배열 예제:


const myArray = ["apple", "banana", "cherry"];

for (const item of myArray) {
  console.log(item); // 출력: apple, banana, cherry
}

문자열 예제:


const myString = "Hello";

for (const char of myString) {
  console.log(char); // 출력: H, e, l, l, o
}

for...of 루프

for...of 루프는 이터러블 객체를 순회하는 강력한 구문입니다. 이는 이터레이터 프로토콜의 복잡성을 자동으로 처리하여 시퀀스의 값에 쉽게 접근할 수 있게 해줍니다.

for...of 루프의 구문은 다음과 같습니다:


for (const element of iterable) {
  // 각 요소에 대해 실행될 코드
}

for...of 루프는 이터러블 객체에서 이터레이터를 가져오고(Symbol.iterator 사용), 이터레이터의 next() 메서드를 donetrue가 될 때까지 반복적으로 호출합니다. 각 순회마다 element 변수에는 next()가 반환한 value 속성이 할당됩니다.

커스텀 이터레이터 만들기

자바스크립트는 내장 이터러블을 제공하지만, 이터레이터 프로토콜의 진정한 힘은 자신만의 자료 구조를 위한 커스텀 이터레이터를 정의할 수 있는 능력에 있습니다. 이를 통해 데이터가 순회되고 접근되는 방식을 제어할 수 있습니다.

커스텀 이터레이터를 만드는 방법은 다음과 같습니다:

  1. 자신만의 커스텀 자료 구조를 나타내는 클래스나 객체를 정의합니다.
  2. 클래스나 객체에 Symbol.iterator 메서드를 구현합니다. 이 메서드는 이터레이터 객체를 반환해야 합니다.
  3. 이터레이터 객체는 valuedone 속성을 가진 객체를 반환하는 next() 메서드를 가져야 합니다.

예제: 간단한 범위를 위한 이터레이터 만들기

숫자 범위를 나타내는 Range라는 클래스를 만들어 보겠습니다. 이터레이터 프로토콜을 구현하여 범위 내의 숫자를 순회할 수 있도록 할 것입니다.


class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  [Symbol.iterator]() {
    let currentValue = this.start;
    const that = this; // 이터레이터 객체 내부에서 사용하기 위해 'this'를 캡처합니다

    return {
      next() {
        if (currentValue <= that.end) {
          return {
            value: currentValue++,
            done: false,
          };
        } else {
          return {
            value: undefined,
            done: true,
          };
        }
      },
    };
  }
}

const myRange = new Range(1, 5);

for (const number of myRange) {
  console.log(number); // 출력: 1, 2, 3, 4, 5
}

설명:

예제: 연결 리스트를 위한 이터레이터 만들기

또 다른 예로 연결 리스트 자료 구조를 위한 이터레이터를 만들어 보겠습니다. 연결 리스트는 노드의 시퀀스로, 각 노드는 값과 리스트의 다음 노드에 대한 참조(포인터)를 포함합니다. 리스트의 마지막 노드는 null(또는 undefined)을 참조합니다.


class LinkedListNode {
    constructor(value, next = null) {
        this.value = value;
        this.next = next;
    }
}

class LinkedList {
    constructor() {
        this.head = null;
    }

    append(value) {
        const newNode = new LinkedListNode(value);
        if (!this.head) {
            this.head = newNode;
            return;
        }

        let current = this.head;
        while (current.next) {
            current = current.next;
        }
        current.next = newNode;
    }

    [Symbol.iterator]() {
        let current = this.head;

        return {
            next() {
                if (current) {
                    const value = current.value;
                    current = current.next;
                    return {
                        value: value,
                        done: false
                    };
                } else {
                    return {
                        value: undefined,
                        done: true
                    };
                }
            }
        };
    }
}

// 사용 예제:
const myList = new LinkedList();
myList.append("London");
myList.append("Paris");
myList.append("Tokyo");

for (const city of myList) {
    console.log(city); // 출력: London, Paris, Tokyo
}

설명:

제너레이터 함수

제너레이터 함수는 이터레이터를 만드는 더 간결하고 우아한 방법을 제공합니다. 이들은 yield 키워드를 사용하여 필요할 때 값을 생성합니다.

제너레이터 함수는 function* 구문을 사용하여 정의됩니다.

예제: 제너레이터 함수를 사용한 이터레이터 만들기

Range 이터레이터를 제너레이터 함수를 사용하여 다시 작성해 보겠습니다:


class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  *[Symbol.iterator]() {
    for (let i = this.start; i <= this.end; i++) {
      yield i;
    }
  }
}

const myRange = new Range(1, 5);

for (const number of myRange) {
  console.log(number); // 출력: 1, 2, 3, 4, 5
}

설명:

제너레이터 함수는 next() 메서드와 done 플래그를 자동으로 처리하여 이터레이터 생성을 단순화합니다.

예제: 피보나치 수열 제너레이터

제너레이터 함수 사용의 또 다른 훌륭한 예는 피보나치 수열을 생성하는 것입니다:


function* fibonacciSequence() {
  let a = 0;
  let b = 1;

  while (true) {
    yield a;
    [a, b] = [b, a + b]; // 동시 업데이트를 위한 구조 분해 할당
  }
}

const fibonacci = fibonacciSequence();

for (let i = 0; i < 10; i++) {
  console.log(fibonacci.next().value); // 출력: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
}

설명:

이터레이터 프로토콜 사용의 이점

고급 이터레이터 기술

이터레이터 결합

여러 이터레이터를 단일 이터레이터로 결합할 수 있습니다. 이는 여러 소스의 데이터를 통합된 방식으로 처리해야 할 때 유용합니다.


function* combineIterators(...iterables) {
  for (const iterable of iterables) {
    for (const item of iterable) {
      yield item;
    }
  }
}

const array1 = [1, 2, 3];
const array2 = ["a", "b", "c"];
const string1 = "XYZ";

const combined = combineIterators(array1, array2, string1);

for (const value of combined) {
  console.log(value); // 출력: 1, 2, 3, a, b, c, X, Y, Z
}

이 예제에서 `combineIterators` 함수는 임의의 수의 이터러블을 인수로 받습니다. 각 이터러블을 순회하며 각 항목을 yield합니다. 그 결과 모든 입력 이터러블의 모든 값을 생성하는 단일 이터레이터가 됩니다.

이터레이터 필터링 및 변환

다른 이터레이터가 생성한 값을 필터링하거나 변환하는 이터레이터를 만들 수도 있습니다. 이를 통해 데이터를 파이프라인 방식으로 처리하며, 생성되는 각 값에 다른 연산을 적용할 수 있습니다.


function* filterIterator(iterable, predicate) {
  for (const item of iterable) {
    if (predicate(item)) {
      yield item;
    }
  }
}

function* mapIterator(iterable, transform) {
  for (const item of iterable) {
    yield transform(item);
    }
}

const numbers = [1, 2, 3, 4, 5, 6];

const evenNumbers = filterIterator(numbers, (x) => x % 2 === 0);
const squaredEvenNumbers = mapIterator(evenNumbers, (x) => x * x);

for (const value of squaredEvenNumbers) {
    console.log(value); // 출력: 4, 16, 36
}

여기서 `filterIterator`는 이터러블과 predicate 함수를 인수로 받습니다. predicate가 `true`를 반환하는 항목만 yield합니다. `mapIterator`는 이터러블과 transform 함수를 인수로 받습니다. 각 항목에 transform 함수를 적용한 결과를 yield합니다.

실제 적용 사례

이터레이터 프로토콜은 자바스크립트 라이브러리와 프레임워크에서 널리 사용되며, 특히 대용량 데이터셋이나 비동기 작업을 다룰 때 다양한 실제 애플리케이션에서 유용합니다.

모범 사례

결론

자바스크립트 이터레이터 프로토콜은 자료 구조를 순회하는 강력하고 유연한 방법을 제공합니다. 이터러블 및 이터레이터 프로토콜을 이해하고 제너레이터 함수를 활용함으로써, 특정 요구에 맞는 커스텀 이터레이터를 만들 수 있습니다. 이를 통해 데이터를 효율적으로 다루고, 코드 가독성을 향상시키며, 애플리케이션의 성능을 높일 수 있습니다. 이터레이터를 마스터하면 자바스크립트의 기능에 대한 더 깊은 이해를 얻고, 더 우아하고 효율적인 코드를 작성할 수 있게 됩니다.