junyeokk
Blog
JavaScript·2026. 01. 25·4

Iterable과 Iterator 프로토콜

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

개요

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로 순회할 수 있는 이유가 바로 이것이다.

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

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

문자열도 마찬가지다:

javascript
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를 꺼내서 확인해보자:

javascript
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()); // ?

결과는 아래와 같다.

iterator next() 호출 결과

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


Iterator 프로토콜

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

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

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


커스텀 Iterable 만들기

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

javascript
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이 출력된다.

커스텀 iterable 실행 결과

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


Generator로 더 간단하게

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

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

for (const num of myIterable) {
  console.log(num);
}

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

generator iterable 실행 결과

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 연산자

javascript
const arr = [1, 2, 3];
console.log([...arr]); // [1, 2, 3]

const str = "hello";
console.log([...str]); // ['h', 'e', 'l', 'l', 'o']
spread 연산자 실행 결과

구조 분해 할당

javascript
const [a, b, c] = [1, 2, 3];
console.log(a, b, c); // 1 2 3
구조 분해 할당 실행 결과

Array.from

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

일반 객체는 Iterable이 아니다

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

javascript
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이다.

javascript
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
Object.entries 실행 결과

Iterable과 Array-like는 다르다

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

javascript
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을 쓰면 값이 아닌 인덱스가 문자열로 나온다:

javascript
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() 등을 활용해야 한다