3.4 이터러블과 이터레이터
이터레이션 프로토콜이란?
ES6에서 도입된 이터레이션 프로토콜은 데이터를 순회하는 통일된 방법을 정의합니다. 이 프로토콜을 따르는 객체는 for...of, 전개 연산자(...), 구조 분해 등에서 사용할 수 있습니다.
두 가지 프로토콜로 구성됩니다:
- 이터러블(Iterable) 프로토콜:
Symbol.iterator메서드를 구현 - 이터레이터(Iterator) 프로토콜:
next()메서드를 가진 객체 반환
이터레이터 프로토콜
이터레이터는 next() 메서드를 가지며, { value, done } 형태의 객체를 반환합니다.
// 이터레이터 수동 사용
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는 이것을 내부적으로 처리
for (const item of [1, 2, 3]) {
console.log(item); // 1, 2, 3
}
기본 이터러블 타입
JavaScript에는 기본적으로 이터러블인 타입들이 있습니다.
// Array
for (const x of [1, 2, 3]) console.log(x);
// String
for (const char of "Hello") console.log(char); // H, e, l, l, o
// Map
const map = new Map([["a", 1], ["b", 2]]);
for (const [key, value] of map) {
console.log(`${key}: ${value}`);
}
// Set
const set = new Set([1, 2, 3]);
for (const x of set) console.log(x);
// arguments 객체 (함수 내)
function sum() {
let total = 0;
for (const n of arguments) total += n;
return total;
}
// NodeList (DOM)
for (const el of document.querySelectorAll("p")) {
el.style.color = "red";
}
// TypedArray
for (const byte of new Uint8Array([72, 101, 108])) {
console.log(byte);
}
커스텀 이터러블 구현
Symbol.iterator 메서드를 구현하면 어떤 객체든 이터러블로 만들 수 있습니다.
// 범위(range) 이터러블
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
if (current <= last) {
return { value: current++, done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
};
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
console.log([...range]); // [1, 2, 3, 4, 5]
const [first, second, ...rest] = range;
console.log(first, second, rest); // 1 2 [3, 4, 5]
// 재사용 가능한 Range 클래스
class Range {
constructor(start, end, step = 1) {
this.start = start;
this.end = end;
this.step = step;
}
[Symbol.iterator]() {
let current = this.start;
const { end, step } = this;
return {
next() {
if (current <= end) {
const value = current;
current += step;
return { value, done: false };
}
return { value: undefined, done: true };
}
};
}
}
const range1 = new Range(0, 10, 2);
console.log([...range1]); // [0, 2, 4, 6, 8, 10]
for (const n of new Range(1, 100, 7)) {
console.log(n); // 1, 8, 15, 22, ...
}
제너레이터(Generator) 기초
제너레이터는 이터레이터를 만드는 더 간단한 방법입니다. function* 문법과 yield 키워드를 사용합니다.
// 기본 제너레이터
function* simpleGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = simpleGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }
// for...of 사용 가능 (제너레이터는 이터러블!)
for (const x of simpleGenerator()) {
console.log(x); // 1, 2, 3
}
// Range를 제너레이터로 간단하게
function* range(start, end, step = 1) {
for (let i = start; i <= end; i += step) {
yield i;
}
}
console.log([...range(1, 5)]); // [1, 2, 3, 4, 5]
console.log([...range(0, 10, 2)]); // [0, 2, 4, 6, 8, 10]
제너레이터의 실행 흐름
제너레이터는 yield에서 일시정지하고, next()가 호출될 때 재개됩니다.
function* stepByStep() {
console.log("1단계 시작");
yield "첫 번째 값";
console.log("2단계 시작");
yield "두 번째 값";
console.log("3단계 시작");
return "완료";
}
const gen = stepByStep();
console.log(gen.next());
// "1단계 시작" 출력
// { value: "첫 번째 값", done: false }
console.log(gen.next());
// "2단계 시작" 출력
// { value: "두 번째 값", done: false }
console.log(gen.next());
// "3단계 시작" 출력
// { value: "완료", done: true }
무한 시퀀스
제너레이터는 끝없이 값을 생성하는 무한 시퀀스를 만들 수 있습니다.
// 무한 자연수 시퀀스
function* naturals() {
let n = 1;
while (true) {
yield n++;
}
}
// 처음 10개만 사용
function take(iterable, n) {
const result = [];
for (const item of iterable) {
result.push(item);
if (result.length >= n) break;
}
return result;
}
console.log(take(naturals(), 10)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// 피보나치 수열
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
console.log(take(fibonacci(), 10)); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
// 무한 순환 이터레이터
function* cycle(arr) {
let i = 0;
while (true) {
yield arr[i % arr.length];
i++;
}
}
const colors = cycle(["빨강", "초록", "파랑"]);
console.log(take(colors, 7));
// ["빨강", "초록", "파랑", "빨강", "초록", "파랑", "빨강"]
yield* — 이터러블 위임
yield*는 다른 이터러블에 순회를 위임합니다.
function* gen1() {
yield 1;
yield 2;
}
function* gen2() {
yield "a";
yield* gen1(); // gen1의 모든 값을 여기서 yield
yield "b";
}
console.log([...gen2()]); // ["a", 1, 2, "b"]
// 트리 순회 예시
function* flattenTree(node) {
yield node.value;
for (const child of node.children || []) {
yield* flattenTree(child); // 재귀적 위임
}
}
const tree = {
value: 1,
children: [
{ value: 2, children: [{ value: 4 }, { value: 5 }] },
{ value: 3, children: [{ value: 6 }] },
],
};
console.log([...flattenTree(tree)]); // [1, 2, 4, 5, 3, 6]
지연 평가 (Lazy Evaluation)
제너레이터의 핵심 이점 중 하나는 지연 평가입니다. 값이 필요할 때만 계산합니다.
// 즉시 평가: 모든 데이터를 한번에 메모리에
const bigArray = Array.from({ length: 1000000 }, (_, i) => i * 2);
const filtered = bigArray.filter(n => n % 6 === 0).slice(0, 5);
// 100만 개 생성 → 필터 → 5개만 사용 (낭비!)
// 지연 평가: 필요한 만큼만 계산
function* generate(count) {
for (let i = 0; i < count; i++) {
yield i * 2;
}
}
function* lazyFilter(iterable, predicate) {
for (const item of iterable) {
if (predicate(item)) yield item;
}
}
function* lazyTake(iterable, n) {
let count = 0;
for (const item of iterable) {
if (count >= n) break;
yield item;
count++;
}
}
// 5개를 찾을 때까지만 계산!
const lazyResult = [...lazyTake(
lazyFilter(generate(1000000), n => n % 6 === 0),
5
)];
console.log(lazyResult); // [0, 6, 12, 18, 24]
고수 팁
제너레이터로 상태 머신 구현
function* trafficLight() {
while (true) {
yield "빨강"; // 정지
yield "초록"; // 진행
yield "노랑"; // 주의
}
}
const light = trafficLight();
// 신호등 전환
console.log(light.next().value); // "빨강"
console.log(light.next().value); // "초록"
console.log(light.next().value); // "노랑"
console.log(light.next().value); // "빨강" (다시 순환)
// 실전: 페이지네이션 제너레이터
async function* paginate(url) {
let page = 1;
while (true) {
const response = await fetch(`${url}?page=${page}`);
const { data, hasMore } = await response.json();
yield* data;
if (!hasMore) break;
page++;
}
}
// 사용: 모든 페이지를 자동으로 순회
for await (const item of paginate('/api/items')) {
console.log(item);
// 각 아이템을 처리, 페이지 전환은 자동
}