junyeokk
Blog
JavaScript·2026. 01. 25·5분

Iterable과 Iterator 프로토콜

for...of, spread, 구조 분해가 작동하는 원리 — Symbol.iterator와 next() 프로토콜을 이해하면 커스텀 순회 객체를 직접 만들 수 있다.

개요

for...of 루프로 배열을 순회할 수 있는 이유는 배열이 iterable이기 때문이다. 이 문서에서는 iterable과 iterator의 동작 원리를 살펴본다. 이를 이해하면 커스텀 iterable을 만들거나, matchAll 같은 메서드가 왜 for...of와 함께 쓰이는지 알 수 있다.


Iterable이란

배열, 문자열, Map, Set은 모두 데이터를 담는 자료구조지만, 내부 구조는 전부 다르다. 만약 자료구조마다 순회 방법이 달라야 한다면 코드가 복잡해진다. JavaScript는 이 문제를 iterable 프로토콜로 해결한다. 자료구조가 정해진 규칙(프로토콜)만 따르면, for...of 하나로 어떤 자료구조든 순회할 수 있다. 직접 만든 자료구조도 이 프로토콜만 구현하면 for...of, spread, 구조 분해와 바로 호환된다.

Iterable은 이 프로토콜을 구현한, 반복 가능한 객체를 말한다. JavaScript에서 for...of 루프로 순회할 수 있으려면 해당 객체가 iterable 프로토콜을 구현해야 한다.

기본적으로 iterable인 객체들이 있다:

  • Array
  • String
  • Map
  • Set
  • TypedArray
  • arguments
  • NodeList

배열과 문자열을 for...of로 순회할 수 있는 이유가 바로 이것이다.

const arr = [1, 2, 3];
for (const item of arr) {
  console.log(item);
}

위 코드를 실행하면 1, 2, 3이 차례로 출력된다. 배열이 iterable이기 때문에 for...of가 동작한다.

문자열도 마찬가지다:

const str = "hello";
for (const char of str) {
  console.log(char);
}

문자열의 각 문자가 순회된다. 그런데 이게 어떻게 가능한 걸까?


Iterable 프로토콜의 동작 원리

객체가 iterable이 되려면 Symbol.iterator 메서드를 구현해야 한다. 이 메서드는 iterator 객체를 반환한다. 여기서 Symbol을 쓰는 이유는 기존 프로퍼티와의 이름 충돌을 피하기 위해서다. 일반 문자열 키 "iterator"를 쓰면 객체에 이미 같은 이름의 프로퍼티가 있을 수 있지만, Symbol은 유일한 값이므로 충돌이 발생하지 않는다.

for...of가 실행되면 내부적으로 다음 과정이 일어난다:

  1. 객체의 [Symbol.iterator]() 메서드를 호출한다
  2. 반환된 iterator 객체의 next() 메서드를 반복 호출한다
  3. next()가 { done: true }를 반환하면 순회를 종료한다

직접 iterator를 꺼내서 확인해보자:

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

결과는 아래와 같다.

for...of는 이 과정을 자동으로 수행한다. 마지막에 done: true가 반환되면 루프가 종료된다.


Iterator 프로토콜

Iterator는 next() 메서드를 가진 객체다. next()는 항상 { value, done } 형태의 객체를 반환해야 한다.

속성설명
value현재 순회 값
done순회 완료 여부. true면 더 이상 값이 없다

이 규칙만 따르면 어떤 객체든 iterable로 만들 수 있다.


커스텀 Iterable 만들기

1부터 3까지 순회하는 커스텀 iterable을 만들어보자:

const myIterable = {
  [Symbol.iterator]() {
    let count = 0;
    return {
      next() {
        count++;
        if (count <= 3) {
          return { value: count, done: false };
        }
        return { value: undefined, done: true };
      },
    };
  },
};
 
for (const num of myIterable) {
  console.log(num);
}

위 코드를 실행하면 1, 2, 3이 출력된다.

[Symbol.iterator]() 메서드가 iterator 객체를 반환한다. 이 iterator의 next() 메서드가 호출될 때마다 count를 증가시키고, 3을 초과하면 done: true를 반환해서 순회를 종료한다.


Generator로 더 간단하게

Generator 함수를 사용하면 iterator를 더 쉽게 만들 수 있다. yield 키워드가 next() 호출마다 값을 반환하고 일시 정지한다.

const myIterable = {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  },
};
 
for (const num of myIterable) {
  console.log(num);
}

앞서 작성한 코드와 동일하게 동작하지만, 코드가 훨씬 간결하다. function* 문법과 yield가 iterator 프로토콜을 자동으로 구현해준다.


Iterable을 소비하는 문법들

for...of만 iterable을 사용하는 게 아니다. 다음 문법들은 모두 내부적으로 Symbol.iterator를 호출한다.

문법동작예시
for...of값을 하나씩 순회for (const x of arr)
Spread (...)iterable을 펼침[...arr], fn(...arr)
구조 분해 할당순서대로 변수에 할당const [a, b] = arr
Array.from()iterable을 배열로 변환Array.from(set)
new Map() / new Set()iterable로 초기화new Set([1, 2, 3])
Promise.all()iterable의 Promise를 대기Promise.all(promises)

각각 실행 결과를 확인해보자.

Spread 연산자

const arr = [1, 2, 3];
console.log([...arr]); // [1, 2, 3]
 
const str = "hello";
console.log([...str]); // ['h', 'e', 'l', 'l', 'o']

구조 분해 할당

const [a, b, c] = [1, 2, 3];
console.log(a, b, c); // 1 2 3

Array.from

const set = new Set([1, 2, 3]);
const arr = Array.from(set);
console.log(arr); // [1, 2, 3]

일반 객체는 Iterable이 아니다

주의할 점이 있다. 일반 객체는 기본적으로 iterable이 아니다.

const obj = { a: 1, b: 2 };
 
for (const item of obj) {
  console.log(item);
}
// TypeError: obj is not iterable

객체를 순회하려면 Object.keys(), Object.values(), Object.entries()를 사용해야 한다. 이 메서드들은 배열을 반환하고, 배열은 iterable이다.

const obj = { a: 1, b: 2 };
 
for (const key of Object.keys(obj)) {
  console.log(key);
}
// 'a', 'b'
 
for (const [key, value] of Object.entries(obj)) {
  console.log(key, value);
}
// 'a' 1
// 'b' 2

Iterable과 Array-like는 다르다

비슷해 보이지만 다른 개념이 하나 더 있다. Array-like 객체는 length 속성과 인덱스 접근(obj[0])이 가능한 객체를 말한다. 하지만 Symbol.iterator가 없으면 iterable은 아니다.

const arrayLike = { 0: "a", 1: "b", length: 2 };
 
// 인덱스 접근은 가능하다
console.log(arrayLike[0]); // 'a'
 
// 하지만 for...of는 안 된다
for (const item of arrayLike) {
  console.log(item);
}
// TypeError: arrayLike is not iterable

Array.from()은 iterable뿐 아니라 array-like 객체도 배열로 변환할 수 있다. 그래서 DOM의 document.querySelectorAll()이 반환하는 NodeList처럼 array-like이면서 iterable인 객체도, 순수 array-like 객체도 모두 Array.from()으로 처리할 수 있다.

for...in과 혼동하지 말 것

for...of와 비슷하게 생긴 for...in이 있다. 둘은 완전히 다르다.

구분for...infor...of
순회 대상객체의 열거 가능한 속성 키iterable의 값
배열에 사용 시인덱스(문자열)를 반환요소 값을 반환
일반 객체사용 가능TypeError

배열에 for...in을 쓰면 값이 아닌 인덱스가 문자열로 나온다:

const arr = ["a", "b", "c"];
 
for (const item in arr) {
  console.log(item, typeof item);
}
// '0' string
// '1' string
// '2' string

for...in은 프로토타입 체인의 속성까지 열거할 수 있어서 배열 순회에는 적합하지 않다. 배열을 순회할 때는 for...of를 사용한다.


정리

  • Iterable은 Symbol.iterator 메서드를 구현한 객체다
  • Iterator는 next() 메서드를 가진 객체이며, { value, done }을 반환한다
  • for...of, spread, 구조 분해 등은 모두 iterable 프로토콜을 사용한다
  • Generator를 사용하면 iterator를 간단하게 구현할 수 있다
  • 일반 객체는 iterable이 아니므로 Object.entries() 등을 활용해야 한다