Iteration protocols

순회 프로토콜은 새로운 내장 객체 또는 구문이 아닌 프로토콜입니다. 이러한 프로토콜은 몇 가지 규칙에 따라 모든 객체에서 구현될 수 있습니다.

프로토콜은 순회 가능 프로토콜반복자 프로토콜로 두 가지가 있습니다.

순회 가능 프로토콜

순회 가능 프로토콜을 사용하면 JavaScript 객체를 for...of 구조에서 반복되는 값과 같은 순회 동작을 정의하거나 사용자 지정할 수 있습니다. Object와는 달리 Array 또는 Map과 같은 일부 내장 유형은 기본 순회 동작이 있는 내장 순회 가능 항목입니다.

순회 가능이 되기위해 객체는 반드시 @@iterator 메서드를 구현해야 합니다. 즉, 객체(또는 프로토타입 체인의 객체 중 하나)에 상수 Symbol.iterator를 통해 이용할 수 있는 @@iterator 키가 있는 속성이 있어야 합니다.

[Symbol.iterator]

반복자 프로토콜을 준수하는 객체를 반환하는 인수가 없는 함수.

객체를 순회해야 할 때마다(예: for...of 루프 시작 시) @@iterator 메서드는 인수 전달 없이 호출되고, 반환된 반복자는 반복할 값을 가져오는 데 사용됩니다.

이 인수가 없는 함수는, 순회 가능 객체의 메서드로 호출됩니다. 따라서 함수 내에서 this 키워드를 사용하여 순회 가능 객체의 속성에 접근하고 순회 중에 제공할 항목을 결정할 수 있습니다.

이 함수는 평범한 함수이거나 제너레이터 함수일 수 있고, 호출 시 반복자 객체가 반환됩니다. 제너레이터 함수 내에서 각 항목은 yield를 사용하여 제공될 수 있습니다.

반복자 프로토콜

반복자 프로토콜은 유한하거나 무한한 일련의 값을 생성하는 표준 방법을 정의하고, 모든 값이 생성되었을 때 잠재적으로 반환 값을 정의합니다.

객체는 다음 의미 체계를 사용하여 next() 메서드를 구현할 때 반복자가 됩니다.

next()

0개 또는 1개의 인수를 허용하고 IteratorResult 인터페이스(아래 참조)를 준수하는 객체를 반환하는 함수입니다. 내장 언어 기능(for...of 등)이 반복자를 사용할 때 객체가 아닌 값(false 또는 undefined 등)이 반환되면 TypeError ("iterator.next() returned a non-object value")가 발생합니다.

모든 반복자 프로토콜 메서드(next(), return(), 그리고 throw())는 IteratorResult 인터페이스를 구현하는 객체를 반환해야 합니다. 다음 속성이 반드시 있어야 합니다.

done Optional

불리언 값으로, 반복자가 시퀀스에서 다음 값을 생성할 수 있는 경우엔 false입니다. (이는 done 속성을 지정하지 않는 것과 같습니다.)

반복자가 시퀀스를 완료한 경우에 이 값은 true입니다. 이때 value는 반복자의 반환 값을 선택적으로 지정합니다.

value Optional

반복자가 반환하는 모든 JavaScript 값입니다. donetrue이면 생략할 수 있습니다.

실제로 두 속성이 모두 엄격하게 요구되진 않습니다. 속성이 없는 객체가 반환되면 사실상 { done: false, value: undefined }와 동일합니다.

반복자가 done: true로 결과를 반환하면 next()에 대한 모든 후속 호출도 done: true를 반환할 것으로 예상되지만 이는 언어 수준에서 강제되지 않습니다.

next 메서드는 메서드 본문에서 사용할 수 있는 값을 받을 수 있습니다. 내장 언어 기능은 어떠한 값도 전달하지 않습니다. 제너레이터next 메서드로 전달된 값은 해당 yield 표현식의 값이 됩니다.

반복자는 선택적으로 return(value)throw(exception) 메서드를 구현할 수 있습니다. 이 메서드는 호출될 때 호출자가 순회를 완료했으며, 필요한 정리(예: 데이터베이스 연결 닫기)를 수행할 수 있음을 반복자에게 알립니다.

return(value) Optional

0개 또는 1개의 인수를 허용하고 IteratorResult 인터페이스를 준수하는 객체를 반환하는 함수입니다. 일반적으로 value는 전달된 vlaue와 같고 donetrue입니다. 이 메서드를 호출하면 반복자에게 호출자가 더 이상 next()를 호출을 할 의도가 없으며, 정리 작업을 수행할 수 있음을 알립니다.

throw(exception) Optional

0개 또는 1개의 인수를 허용하고 IteratorResult 인터페이스를 준수하는 객체를 반환하는 함수입니다. 일반적으로 donetrue입니다. 이 메서드를 호출하면 반복자에게 호출자가 오류 조건을 감지했음을 알리며, exception은 일반적으로 Error 인스턴스입니다.

참고: 특정 객체가 반복자 프로토콜을 구현하는지 여부를 반사적으로(즉, 실제로 next()를 호출하고 반환된 결과를 확인하지 않고는) 알 수 없습니다.

반복자를 순회 가능으로 만드는 것은 매우 쉽습니다. this를 반환하는 [@@iterator]() 메서드를 구현하기만 하면 됩니다.

js
// 반복자 프로토콜과 순회 가능을 모두 만족
const myIterator = {
  next() {
    // ...
  },
  [Symbol.iterator]() {
    return this;
  },
};

이러한 객체를 순회 가능 반복자라고 합니다. 이렇게 하면 순회 가능을 기대하는 다양한 구문에서 반복자를 사용할 수 있습니다. 따라서 순회 가능을 구현하지 않고 반복자 프로토콜을 구현하는 것은 거의 유용하지 않습니다. (실제로 거의 모든 구문과 API는 반복자가 아니라 순회 가능을 기대합니다.) 제너레이터 객체가 그 예입니다.

js
const aGeneratorObject = (function* () {
  yield 1;
  yield 2;
  yield 3;
})();

console.log(typeof aGeneratorObject.next);
// "function" — next 메서드(올바른 결과를 반환)가 있으므로 반복자입니다.

console.log(typeof aGeneratorObject[Symbol.iterator]);
// "function" — @@iterator 메서드(올바른 반복자를 반환)가 있으므로 순회 가능입니다.

console.log(aGeneratorObject[Symbol.iterator]() === aGeneratorObject);
// true — @@iterator 메서드는 자신(반복자)을 반환하므로 순회 가능 반복자입니다.

모든 내장 반복자는 this를 반환하는 [@@iterator]() 메서드를 구현하는 Iterator.prototype (en-US)에서 상속되었기 때문에 내장 반복자도 순회 가능입니다.

그러나 가능하다면 iterable[Symbol.iterator]Set.prototype[@@iterator]()처럼 항상 처음부터 시작하는 별개의 반복자를 반환하는 것이 좋습니다.

비동기 반복자와 비동기 순회 가능 프로토콜

비동기 반복자비동기 순회 가능 프로토콜 이라는 비동기 순회에 사용되는 또 다른 프로토콜 쌍이 있습니다. 순회 가능과 반복자 프로토콜과 비교했을 때 매우 유사한 인터페이스를 가지고 있고, 반복자 메서드 호출의 각 반환 값이 프로미스로 래핑된다는 점만 다릅니다.

객체는 다음 메서드를 구현할 때 비동기 순회 가능 프로토콜을 구현합니다.

[Symbol.asyncIterator] (en-US)

비동기 반복자 프로토콜을 준수하는 객체를 반환하는 인수가 없는 함수입니다.

객체는 다음 메서드를 구현할 때 비동기 반복자 프로토콜을 구현합니다.

next()

0개 또는 1개의 인수를 허용하고 프로미스를 반환하는 함수입니다. 프로미스는 IteratorResult 인터페이스를 준수하는 객체로 이행되며, 속성은 동기 반복자와 동일한 의미 체계를 갖습니다.

return(value) Optional

0개 또는 1개의 인수를 허용하고 프로미스를 반환하는 함수입니다. 프로미스는 IteratorResult 인터페이스를 준수하는 객체로 이행되며, 속성은 동기 반복자와 동일한 의미 체계를 갖습니다.

throw(exception) Optional

0개 또는 1개의 인수를 허용하고 프로미스를 반환하는 함수입니다. 프로미스는 IteratorResult 인터페이스를 준수하는 객체로 이행되며, 속성은 동기 반복자와 동일한 의미 체계를 갖습니다.

언어와 순회 프로토콜 간의 상호 작용

언어는 순회 가능과 반복자를 생성하거나 소비하는 API를 지정합니다.

내장 순회 가능

String, Array, TypedArray, Map, Set, 그리고 Intl.Segmenter.prototype.segment() (en-US)에서 반환된 Segments (en-US)는 각각의 prototype 객체가 @@iterator 메서드를 구현하기 때문에 모두 내장 순회 가능입니다. 또한 인수 객체와 NodeList와 같은 일부 DOM 컬렉션 유형도 순회 가능입니다. ReadableStream은 이 문서를 작성할 당시의 유일한 내장 비동기 순회 가능입니다.

제너레이터 함수는 순회 가능 반복자인 제너레이터 객체를 반환합니다. 비동기 제너레이터 함수 (en-US)는 비동기 순회 가능 반복자인 비동기 제너레이터 객체 (en-US)를 반환합니다.

내장 순회 가능에서 반환된 반복자는 실제로 모두 앞서 언급한 [Symbol.iterator]() { return this; } 메서드를 사용하여 순회 가능 반복자로 만드는 공통 클래스(현재 노출되어있지 않음)를 상속받습니다. 앞으로 이러한 내장 반복자는 next() 메서드 외에 추가적으로 반복자 프로토콜에서 필요한 도우미 메서드를 가지고 있을 수 있습니다. 그래픽 콘솔에 로그하여 반복자의 프로토타입 체인을 검사해볼 수 있습니다.

console.log([][Symbol.iterator]());

Array Iterator {}
  [[Prototype]]: Array Iterator     ==> 모든 배열 반복자가 공유하는 프로토타입입니다.
    next: ƒ next()
    Symbol(Symbol.toStringTag): "Array Iterator"
    [[Prototype]]: Object           ==> 모든 내장 반복자가 공유하는 프로토타입입니다.
      Symbol(Symbol.iterator): ƒ [Symbol.iterator]()
      [[Prototype]]: Object         ==> Object.prototype 입니다.

순회 가능을 허용하는 내장 API

순회 가능을 허용하는 많은 API가 있습니다. 몇 가지 예는 다음과 같습니다.

js
const myObj = {};

new WeakSet(
  (function* () {
    yield {};
    yield myObj;
    yield {};
  })(),
).has(myObj); // true

순회 가능을 기대하는 구문

for...of 루프, 배열 및 매개변수 전개, yield*, 그리고 배열 구조 분해와 같은 일부 명령문과 표현식에는 순회 가능이 필요합니다.

js
for (const value of ["a", "b", "c"]) {
  console.log(value);
}
// "a"
// "b"
// "c"

console.log([..."abc"]); // ["a", "b", "c"]

function* gen() {
  yield* ["a", "b", "c"];
}

console.log(gen().next()); // { value: "a", done: false }

[a, b, c] = new Set(["a", "b", "c"]);
console.log(a); // "a"

내장 구문이 반복자를 순회하고 마지막 결과의 donefalse(즉, 반복자가 더 많은 값을 생성할 수 있음)이지만 더 이상 값이 필요하지 않은 경우 return 메서드가 있는경우, 해당 메서드가 호출됩니다. 예를 들어 break 또는 returnfor...of 루프에서 발생하거나 모든 식별자가 배열 구조 분해에 이미 바인딩된 경우에 이러한 상황이 발생할 수 있습니다.

js
const obj = {
  [Symbol.iterator]() {
    let i = 0;
    return {
      next() {
        i++;
        console.log("Returning", i);
        if (i === 3) return { done: true, value: i };
        return { done: false, value: i };
      },
      return() {
        console.log("Closing");
        return { done: true };
      },
    };
  },
};

const [b] = obj;
// Returning 1
// Closing

const [a, b, c] = obj;
// Returning 1
// Returning 2
// Returning 3
// 이미 끝에 도달했기 때문에(마지막 호출이 `done: true`를 반환)
// `return`이 호출되지 않습니다.

for (const b of obj) {
  break;
}
// Returning 1
// Closing

비동기 제너레이터 함수 (en-US) (동기 제너레이터 함수는 아님)의 for await...of 루프와 yield*는 비동기 순회 가능과 상호 작용하는 유일한 방법입니다. for...of, 배열 전개 등을 동기 순회 가능도 아닌 비동기 순회 가능과 사용하면(즉, [@@asyncIterator]()는 있지만 [@@iterator]()는 없음) TypeError(x is not iterable.)가 발생합니다.

잘못 구성된 순회 가능

순회 가능의 @@iterator 메서드가 반복자 객체를 반환하지 않으면 잘못 구성된 순회 가능으로 간주됩니다.

이렇게 사용하면 런타임 오류 또는 버그가 발생할 수 있습니다.

js
const nonWellFormedIterable = {};
nonWellFormedIterable[Symbol.iterator] = () => 1;
[...nonWellFormedIterable]; // TypeError: [Symbol.iterator]() returned a non-object value

예제

사용자 정의 순회 가능

여러분은 다음과 같이 여러분만의 순회 가능을 만들 수 있습니다.

js
const myIterable = {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  },
};

console.log([...myIterable]); // [1, 2, 3]

간단한 반복자

반복자는 본래 stateful입니다. 위의 예와 같이 제너레이터 함수로 정의하지 않으면, 여러분은 상태를 클로저에 캡슐화하고 싶을 것입니다.

js
function makeIterator(array) {
  let nextIndex = 0;
  return {
    next() {
      return nextIndex < array.length
        ? {
            value: array[nextIndex++],
            done: false,
          }
        : {
            done: true,
          };
    },
  };
}

const it = makeIterator(["yo", "ya"]);

console.log(it.next().value); // 'yo'
console.log(it.next().value); // 'ya'
console.log(it.next().done); // true

무한 반복자

js
function idMaker() {
  let index = 0;
  return {
    next() {
      return {
        value: index++,
        done: false,
      };
    },
  };
}

const it = idMaker();

console.log(it.next().value); // 0
console.log(it.next().value); // 1
console.log(it.next().value); // 2
// ...

제너레이터로 반복자 정의하기

js
function* makeSimpleGenerator(array) {
  let nextIndex = 0;
  while (nextIndex < array.length) {
    yield array[nextIndex++];
  }
}

const gen = makeSimpleGenerator(["yo", "ya"]);

console.log(gen.next().value); // 'yo'
console.log(gen.next().value); // 'ya'
console.log(gen.next().done); // true

function* idMaker() {
  let index = 0;
  while (true) {
    yield index++;
  }
}

const it = idMaker();

console.log(it.next().value); // 0
console.log(it.next().value); // 1
console.log(it.next().value); // 2
// ...

클래스로 반복자 정의하기

상태 캡슐화는 프라이빗 속성으로도 수행할 수 있습니다.

js
class SimpleClass {
  #data;

  constructor(data) {
    this.#data = data;
  }

  [Symbol.iterator]() {
    // 각 반복자에 대해 새 색인을 사용하세요.
    // 이렇게 하면 break 사용 또는 동일한 순회 가능의 중첩 루프와 같은
    // 사소한 상황에서 순회 가능에 대한 여러 반복이 안전해집니다.
    let index = 0;

    return {
      // 참고: 화살표 함수를 사용하면 `this`가 `next()` 대신
      // `[@@iterator]()` 중 하나를 가리킬 수 있습니다.
      next: () => {
        if (index < this.#data.length) {
          return { value: this.#data[index++], done: false };
        } else {
          return { done: true };
        }
      },
    };
  }
}

const simple = new SimpleClass([1, 2, 3, 4, 5]);

for (const val of simple) {
  console.log(val); // 1 2 3 4 5
}

내장 순회 가능 덮어쓰기

예를 들어 String은 내장 순회 가능 객체입니다.

js
const someString = "hi";
console.log(typeof someString[Symbol.iterator]); // "function"

String기본 반복자 (en-US)는 다음과 같이 문자열의 코드 포인트를 하나씩 반환합니다.

js
const iterator = someString[Symbol.iterator]();
console.log(`${iterator}`); // "[object String Iterator]"

console.log(iterator.next()); // { value: "h", done: false }
console.log(iterator.next()); // { value: "i", done: false }
console.log(iterator.next()); // { value: undefined, done: true }

여러분은 여러분만의 @@iterator를 제공하여 순회 동작을 재정의할 수 있습니다.

js
// auto-boxing을 피하기 위해 명시적으로 String 객체를 구성해야 합니다.
const someString = new String("hi");

someString[Symbol.iterator] = function () {
  return {
    // 단일 요소(문자열 "bye")를 반환하는 반복자 객체
    next() {
      return this._first
        ? { value: "bye", done: (this._first = false) }
        : { done: true };
    },
    _first: true,
  };
};

@@iterator 재정의가 순회 프로토콜을 사용하는 내장 생성 동작에 어떤 영향을 미치는지 확인해보세요.

js
console.log([...someString]); // ["bye"]
console.log(`${someString}`); // "hi"

명세서

Specification
ECMAScript Language Specification
# sec-iteration

같이 보기