자바스크립트 중급 #3 이터레이터와 제너레이터
#2 비동기 입문 다음, 이번엔 자바스크립트의 또 다른 핵심 도구 — 이터레이터와 제너레이터를 정리합니다.
for...of는 어떻게 동작하는가?
#
기초 #3 제어 흐름 에서 for...of로 배열을 순회했습니다.
for (const x of [1, 2, 3]) {
console.log(x);
}이게 배열에서만 동작하는 게 아닙니다. 자바스크립트는 객체가 일정한 약속(이터러블 프로토콜)을 따르면 모두 for...of로 순회할 수 있게 만들어 두었습니다.
for (const ch of 'hello') { // 문자열
console.log(ch);
}
for (const [k, v] of new Map([['a', 1], ['b', 2]])) { // Map
console.log(k, v);
}
for (const x of new Set([1, 2, 3])) { // Set
console.log(x);
}배열, 문자열, Map, Set, 그리고 NodeList(document.querySelectorAll) 등이 이미 이터러블입니다. 모두 같은 약속을 따라요.
이터러블 프로토콜 #
객체가 이터러블이려면 다음 한 가지를 가지고 있어야 합니다.
Symbol.iterator라는 키에 함수가 있어야 한다. 그 함수를 호출하면next()를 가진 이터레이터를 반환해야 한다.next()는{ value, done }객체를 반환해야 한다.
직접 만들어 보면 이해가 빠릅니다.
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 };
}
return { value: undefined, done: true };
},
};
},
};
for (const n of range) {
console.log(n);
}
// 1, 2, 3, 4, 5
Symbol.iterator가 핵심입니다. 키 위치에 [...]가 들어간 건 기초 #5의 계산된 속성 문법입니다. Symbol.iterator 라는 특별한 심볼을 키로 써요.
for...of는 내부적으로 이 약속을 따라 next()를 반복 호출하면서 done이 true가 될 때까지 값을 꺼내 갑니다.
Spread와 디스트럭처링도 이터러블 #
for...of 외에 spread와 배열 디스트럭처링도 이터러블 프로토콜을 사용합니다.
const arr = [...range];
// [1, 2, 3, 4, 5]
const [first, second] = range;
// first = 1, second = 2
Math.max(...range); // 5
Array.from(range); // [1, 2, 3, 4, 5]
이게 강력합니다 — 한 번 이터러블로 만들면 자바스크립트의 모든 시퀀스 도구를 그대로 쓸 수 있습니다.
제너레이터 — 이터러블을 짧게 만드는 문법 #
위에서 만든 range가 동작은 하지만, 이터러블 만드는 코드가 길고 복잡합니다. 자바스크립트에는 같은 일을 한 번에 해주는 문법이 있습니다 — 제너레이터.
function* range(from, to) {
for (let n = from; n <= to; n++) {
yield n;
}
}
for (const n of range(1, 5)) {
console.log(n);
}
// 1, 2, 3, 4, 5
[...range(1, 5)]; // [1, 2, 3, 4, 5]
문법:
function*— 별표가 붙은 함수 → 제너레이터yield— 한 값을 내어주고 멈춤- 다시 호출되면
yield다음 줄부터 계속
제너레이터 함수를 호출하면 이터러블한 객체가 반환됩니다. for...of가 그 객체에서 값을 하나씩 꺼내 갑니다.
게으른 시퀀스 — 무한 시퀀스도 가능 #
제너레이터의 진짜 강점은 필요한 만큼만 만든다는 점입니다. 무한 시퀀스도 표현 가능합니다.
function* naturals() {
let n = 1;
while (true) {
yield n++;
}
}
const gen = naturals();
gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }
gen.next(); // { value: 3, done: false }
// ... 영원히
while (true)가 무한 루프지만 멈추지 않습니다. next()를 부를 때마다 다음 값만 만듭니다. 호출 시점에만 계산 — 이게 게으름(lazy)의 의미입니다.
무한 시퀀스 잘라 쓰기 #
function* take(iterable, n) {
let count = 0;
for (const x of iterable) {
if (count >= n) return;
yield x;
count++;
}
}
const first10 = [...take(naturals(), 10)];
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
이게 함수형 라이브러리들이 흔히 제공하는 take 함수의 정체입니다.
제너레이터의 다른 활용 #
1) 트리/그래프 순회 #
function* walk(node) {
yield node.value;
for (const child of node.children ?? []) {
yield* walk(child); // 다른 제너레이터 위임
}
}
const tree = {
value: 'root',
children: [
{ value: 'a', children: [{ value: 'a1' }] },
{ value: 'b' },
],
};
for (const v of walk(tree)) {
console.log(v);
}
// root, a, a1, b
yield*는 다른 이터러블의 값을 통째로 흘려보내는 문법. 재귀적인 순회를 깔끔하게 표현할 수 있습니다.
2) 단계별 진행하는 작업 #
function* steps() {
console.log('1단계 시작');
yield;
console.log('2단계 시작');
yield;
console.log('3단계 끝');
}
const s = steps();
s.next(); // 1단계 시작
s.next(); // 2단계 시작
s.next(); // 3단계 끝
호출자가 명시적으로 next()를 부르며 단계를 진행. 테스트나 시뮬레이션에서 종종 활용됩니다.
비동기 이터레이터 — for await...of
#
비동기 데이터를 순회할 때 쓰는 변형. 스트리밍 데이터(파일, fetch 응답 chunk)를 다룰 때 만나요.
async function readChunks(response) {
for await (const chunk of response.body) {
console.log(chunk);
}
}자세한 사용은 다음 #6 fetch API 에서 다룹니다.
Symbol이 무엇인가?
#
Symbol.iterator가 갑자기 등장했는데, Symbol도 자바스크립트의 원시 타입 중 하나입니다. 유일한 식별자를 만들어주는 값입니다.
const s1 = Symbol('id');
const s2 = Symbol('id');
s1 === s2; // false — 같은 설명이라도 다른 심볼
언어 차원의 키(Symbol.iterator, Symbol.asyncIterator 등)는 절대 충돌하지 않게 만들기 위해 Symbol로 정의되어 있습니다. 일반 코드에서는 자주 만들 일이 없지만, 라이브러리가 메타데이터를 객체에 붙일 때 충돌 없이 쓰는 경우가 종종 있습니다.
마무리 #
이번 글에서 정리한 내용:
for...of/ spread / 디스트럭처링은 이터러블 프로토콜 위에서 동작- 이터러블이 되려면
Symbol.iterator로next()를 가진 이터레이터를 반환 - 제너레이터(
function*,yield)로 이터러블을 짧게 만들기 - 게으른 시퀀스 — 무한 시퀀스도 표현 가능
yield*로 다른 이터러블 위임for await...of로 비동기 이터러블 순회- Symbol은 충돌하지 않는 유일한 식별자
다음 글(#4 디스트럭처링과 spread/rest 깊이)에서는 기초에서 가볍게 다룬 디스트럭처링과 spread를 더 깊이 — 중첩 패턴, 매개변수 디스트럭처링, 동적 키 등 자주 만나는 실전 패턴들을 정리합니다.