이터레이터와 제너레이터
우리는 앞서 6장에서 배열의 forEach
, map
, filter
와 같은 고차 함수를 통해 데이터를 반복하고 처리하는 편리한 방법을 배웠습니다. 하지만 이러한 메서드들은 오직 배열에만 적용할 수 있습니다. 자바스크립트에는 배열 외에도 Map
, Set
, String
등 다양한 '반복 가능한(iterable)' 데이터 구조가 존재합니다. 이 모든 데이터 구조를 일관된 방식으로 순회하고 싶다면 어떻게 해야 할까요?
이 질문에 대한 답이 바로 이터레이터(Iterator) 와 제너레이터(Generator) 입니다. 이터레이터는 자바스크립트의 모든 반복 가능한 객체를 표준화된 방식으로 순회할 수 있도록 해주는 프로토콜이며, 제너레이터는 이 이터레이터를 쉽게 생성할 수 있도록 돕는 특별한 함수입니다. 특히 제너레이터는 함수의 실행을 일시 중지하고 재개하는 능력을 통해 비동기 프로그래밍과 복잡한 데이터 스트림 처리에 새로운 가능성을 열어줍니다.
이번 장에서는 이터레이터와 제너레이터의 개념부터 시작하여, 어떻게 동작하고 어떤 상황에서 유용하게 사용될 수 있는지 상세히 알아보겠습니다.
이터레이터 (Iterator) 프로토콜
이터레이터 프로토콜은 자바스크립트의 객체가 for...of
루프와 같은 반복 가능한 구문에서 어떻게 동작해야 하는지를 정의하는 규칙입니다. 어떤 객체가 이터레이터 프로토콜을 따른다는 것은, 그 객체가 Symbol.iterator
라는 특수 메서드를 가지고 있다는 의미입니다.
이터러블 (Iterable) 객체
이터러블(Iterable) 객체는 Symbol.iterator
라는 메서드를 프로토타입 체인에 가지고 있는 객체를 말합니다. 이 Symbol.iterator
메서드는 호출될 때 이터레이터(Iterator) 객체를 반환합니다.
자바스크립트의 내장 이터러블 객체
Array
String
Map
Set
NodeList
(DOM API)arguments
객체
for...of
루프는 이터러블 객체를 순회할 때 내부적으로 이터레이터 프로토콜을 사용합니다.
const myArray = [1, 2, 3];
const myString = "hello";
const mySet = new Set([1, 2, 3]);
// 이터러블은 for...of로 순회 가능
for (const item of myArray) {
console.log(item); // 1, 2, 3
}
for (const char of myString) {
console.log(char); // h, e, l, l, o
}
for (const item of mySet) {
console.log(item); // 1, 2, 3
}
이터레이터 (Iterator) 객체
이터레이터 객체는 next()
라는 메서드를 가지고 있는 객체입니다. next()
메서드를 호출할 때마다 { value: any, done: boolean }
형태의 객체를 반환합니다.
value
: 순회 과정에서 현재 반환되는 값.done
: 순회가 완료되었는지 여부를 나타내는 불리언 값.true
이면 순회가 끝났고, 더 이상 반환할 값이 없음을 의미합니다.
const numbers = [10, 20, 30];
// 배열의 Symbol.iterator 메서드를 호출하여 이터레이터 객체를 얻습니다.
const iterator = numbers[Symbol.iterator]();
console.log(iterator.next()); // 결과: { value: 10, done: false }
console.log(iterator.next()); // 결과: { value: 20, done: false }
console.log(iterator.next()); // 결과: { value: 30, done: false }
console.log(iterator.next()); // 결과: { value: undefined, done: true } (더 이상 요소가 없음)
사용자 정의 이터러블 만들기
우리가 만든 객체도 Symbol.iterator
메서드를 구현하면 for...of
루프를 사용할 수 있는 이터러블로 만들 수 있습니다.
// 숫자 범위를 나타내는 사용자 정의 이터러블 객체
const range = {
from: 1,
to: 5,
[Symbol.iterator]: function() { // Symbol.iterator 메서드를 구현합니다.
let current = this.from; // 클로저처럼 from과 to를 기억합니다.
const last = this.to;
return { // 이터레이터 객체를 반환합니다.
next() { // 이터레이터는 next() 메서드를 가져야 합니다.
if (current <= last) {
return { done: false, value: current++ };
} else {
return { done: true };
}
}
};
}
};
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
// 직접 이터레이터 사용도 가능
const rangeIterator = range[Symbol.iterator]();
console.log(rangeIterator.next()); // { value: 1, done: false }
이터레이터 프로토콜을 이해하면, for...of
루프가 내부적으로 어떻게 동작하는지 알 수 있고, 사용자 정의 데이터 구조도 유연하게 반복할 수 있게 됩니다.
제너레이터 (Generator)
제너레이터(Generator) 는 이터레이터 프로토콜을 훨씬 더 쉽게 구현할 수 있도록 도와주는 특별한 종류의 함수입니다. 제너레이터 함수는 실행을 일시 중지하고 재개할 수 있는 능력을 가지고 있으며, yield
키워드를 사용하여 값을 '생성(yield)'해낼 수 있습니다.
제너레이터 함수 선언
제너레이터 함수는 function*
문법으로 선언합니다. 일반 함수와 달리, 제너레이터 함수를 호출하면 즉시 실행되는 것이 아니라 제너레이터 객체(Generator Object) 를 반환합니다. 이 제너레이터 객체는 이터레이터이자 이터러블입니다.
function* simpleGenerator() {
console.log("제너레이터 시작");
yield 1; // 첫 번째 값을 생성하고 여기서 일시 중지
console.log("첫 번째 yield 후");
yield 2; // 두 번째 값을 생성하고 일시 중지
console.log("두 번째 yield 후");
return 3; // 마지막 값 (done: true)
console.log("return 후 (실행 안 됨)"); // return 이후 코드는 실행되지 않음
}
const generator = simpleGenerator(); // 함수 호출 시 제너레이터 객체 반환
console.log("제너레이터 함수 호출됨, 하지만 아직 실행 안 됨");
console.log(generator.next()); // '제너레이터 시작' 출력, { value: 1, done: false } 반환
console.log(generator.next()); // '첫 번째 yield 후' 출력, { value: 2, done: false } 반환
console.log(generator.next()); // '두 번째 yield 후' 출력, { value: 3, done: true } 반환
console.log(generator.next()); // { value: undefined, done: true } 반환
yield
키워드
yield
키워드는 제너레이터 함수 내부에서 사용됩니다.yield
는 함수의 실행을 일시 중지하고,yield
뒤에 오는 값을 이터레이터의value
로 반환합니다.- 다음에
next()
가 호출되면, 함수는yield
가 있었던 지점부터 실행을 재개합니다.
for...of
루프와 제너레이터
제너레이터 객체는 이터러블이기 때문에 for...of
루프에서 직접 사용할 수 있습니다.
function* countGenerator() {
for (let i = 1; i <= 5; i++) {
yield i;
}
}
for (const num of countGenerator()) {
console.log(num); // 1, 2, 3, 4, 5
}
next()
메서드로 값 전달하기
generator.next(value)
를 호출할 때 인자를 전달하면, 이 값은 이전 yield
표현식의 결과값으로 전달됩니다. 이를 통해 제너레이터의 내부 동작에 외부에서 값을 주입하여 제어할 수 있습니다.
function* multiplyGenerator() {
console.log("제너레이터 시작");
const num1 = yield; // 첫 번째 yield는 값을 받기 위해 대기
console.log(`첫 번째 받은 값: ${num1}`);
const num2 = yield; // 두 번째 yield는 값을 받기 위해 대기
console.log(`두 번째 받은 값: ${num2}`);
return num1 * num2;
}
const multiplier = multiplyGenerator();
console.log(multiplier.next()); // 결과: 제너레이터 시작, { value: undefined, done: false }
console.log(multiplier.next(5)); // 결과: 첫 번째 받은 값: 5, { value: undefined, done: false }
console.log(multiplier.next(10)); // 결과: 두 번째 받은 값: 10, { value: 50, done: true }
console.log(multiplier.next()); // 결과: { value: undefined, done: true }
이 기능은 나중에 async/await
의 내부 동작 원리를 이해하는 데 중요한 힌트가 됩니다.
yield*
(Yield Delegation)
yield*
는 다른 이터러블(또는 제너레이터)을 위임하여 순회할 때 사용합니다.
function* generatorA() {
yield 1;
yield 2;
}
function* generatorB() {
yield 3;
yield* generatorA(); // generatorA의 모든 yield 값을 내보냄
yield 4;
}
for (const value of generatorB()) {
console.log(value); // 3, 1, 2, 4
}
제너레이터의 활용 사례
-
무한 스크롤 / 무한 데이터 스트림: 필요한 만큼만 데이터를 생성하여 메모리 사용을 효율적으로 관리할 수 있습니다.
function* infiniteSequence() { let i = 0; while (true) { // 무한 루프 yield i++; } } const seq = infiniteSequence(); console.log(seq.next().value); // 0 console.log(seq.next().value); // 1 // 필요한 만큼만 값을 가져오므로 무한히 생성 가능
-
비동기 프로그래밍 (과거의 코루틴 구현):
async/await
가 도입되기 전에는 제너레이터를 사용하여 비동기 코드를 동기적으로 보이는 것처럼 작성하는 패턴(코루틴)이 유행했습니다. (co
라이브러리 등)// (개념적인 예시, 실제 async/await와 비교) function* fetchUserAndPosts() { const user = yield fetch('/api/user').then(res => res.json()); const posts = yield fetch(`/api/posts?userId=${user.id}`).then(res => res.json()); return { user, posts }; } // 이런 제너레이터는 특정 실행기(runner)가 next()를 반복적으로 호출하며 Promise를 처리해줘야 함 // 이것이 async/await로 발전함
-
상태 관리 (State Management): 유한 상태 머신(Finite State Machine)과 같이 상태 변화를 제어하는 데 활용될 수 있습니다.
-
이터러블 객체 생성의 편의성: 사용자 정의 객체를
for...of
로 순회할 수 있도록 만들 때, 이터레이터를 직접 구현하는 것보다 제너레이터를 사용하는 것이 훨씬 간단합니다.
마무리하며
이번 장에서는 자바스크립트에서 반복 가능한 객체를 다루는 표준인 이터레이터 프로토콜과, 이 이터레이터를 쉽게 생성하고 함수의 실행 흐름을 제어할 수 있는 강력한 기능인 제너레이터에 대해 학습했습니다.
여러분은 모든 반복 가능한 내장 객체(배열, 문자열, 맵 등)가 이터러블 프로토콜을 따른다는 것을 이해했으며, Symbol.iterator
와 next()
메서드를 통해 직접 이터레이터를 다루거나 사용자 정의 이터러블을 만드는 방법을 배웠습니다.
더 나아가, function*
문법과 yield
키워드를 사용하여 제너레이터 함수를 만들고, next()
를 통해 실행을 일시 중지하고 재개하는 방법을 익혔습니다. 또한, yield*
를 이용한 위임과 next()
로 값을 전달하는 고급 기능, 그리고 무한 스크롤, 비동기 코루틴 등의 활용 사례를 통해 제너레이터의 잠재력을 엿보았습니다.
제너레이터는 async/await
의 등장으로 비동기 처리의 주요 패턴에서는 다소 밀려났지만, 여전히 데이터 스트림 처리나 특정 로직의 실행 흐름을 세밀하게 제어해야 할 때 매우 유용한 도구입니다. 이터레이터와 제너레이터는 자바스크립트의 깊은 내부 동작을 이해하고, 더 유연하고 효율적인 코드를 작성하는 데 큰 도움이 될 것입니다.