Iterable과 Iterator 프로토콜
개요
for...of 루프로 배열을 순회할 수 있는 이유는 배열이 iterable이기 때문이다. 이 문서에서는 iterable과 iterator의 동작 원리를 살펴본다. 이를 이해하면 커스텀 iterable을 만들거나, matchAll 같은 메서드가 왜 for...of와 함께 쓰이는지 알 수 있다.
Iterable이란
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 객체를 반환한다.
for...of가 실행되면 내부적으로 다음 과정이 일어난다:
- 객체의
[Symbol.iterator]()메서드를 호출한다 - 반환된 iterator 객체의
next()메서드를 반복 호출한다 next()가{ done: true }를 반환하면 순회를 종료한다
직접 iterator를 꺼내서 확인해보자:
const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
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의 다양한 활용
Iterable은 for...of 외에도 여러 곳에서 활용된다.
Spread 연산자
spread 연산자(...)는 iterable을 펼친다:
const arr = [1, 2, 3];
console.log([...arr]); // [1, 2, 3]
const str = "hello";
console.log([...str]); // ['h', 'e', 'l', 'l', 'o']
내부적으로 Symbol.iterator를 호출해서 모든 값을 꺼낸다.
구조 분해 할당
구조 분해 할당도 iterable 프로토콜을 사용한다:
const [a, b, c] = [1, 2, 3];
console.log(a, b, c); // 1 2 3
Array.from
Array.from()은 iterable을 배열로 변환한다:
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은
Symbol.iterator메서드를 구현한 객체다 - Iterator는
next()메서드를 가진 객체이며,{ value, done }을 반환한다 for...of, spread, 구조 분해 등은 모두 iterable 프로토콜을 사용한다- Generator를 사용하면 iterator를 간단하게 구현할 수 있다
- 일반 객체는 iterable이 아니므로
Object.entries()등을 활용해야 한다