2025-09-22 23:34
-
자바스크립트 이터레이터와 제너레이터는 데이터 컬렉션을 순회하는 일관된 방식을 제공하여 코드의 복잡성을 줄이고 가독성을 높입니다.
-
이터레이터는
next()메서드를 통해 데이터 조각을 순차적으로 반환하는 객체이며, 제너레이터는function*문법을 사용하여 이러한 이터레이터를 더 쉽게 만들 수 있는 특별한 함수입니다. -
이 두 가지 기능을 활용하면 비동기 처리, 무한 데이터 스트림, 지연 평가 등 복잡한 프로그래밍 패턴을 우아하게 구현할 수 있어 모던 자바스크립트 개발의 핵심 요소로 자리 잡았습니다.
자바스크립트 이터레이터와 제너레이터 완벽 정복 핸드북
자바스크립트로 개발을 하다 보면 배열, 객체, 맵, 셋 등 다양한 형태의 데이터 묶음, 즉 ‘컬렉션’을 다루는 일은 피할 수 없는 숙명과도 같습니다. 이 데이터들을 하나씩 꺼내어 처리해야 할 때, 우리는 보통 for 루프나 forEach 메서드를 사용하곤 하죠. 하지만 데이터의 종류가 다양해지고 구조가 복잡해질수록 순회하는 방식 또한 제각각이 되어 코드의 통일성을 해치고 복잡도를 높이는 원인이 되기도 합니다.
만약 모든 데이터 구조를 똑같은 방식으로 순회할 수 있다면 어떨까요? 마치 어떤 종류의 문이든 하나의 열쇠로 열 수 있는 것처럼 말입니다. 바로 이러한 아이디어에서 출발한 것이 자바스크립트의 **이터레이터(Iterator)**와 **제너레이터(Generator)**입니다. 이들은 데이터 순회에 대한 통일된 규칙, 즉 ‘프로토콜’을 제시함으로써 개발자가 더욱 예측 가능하고 효율적인 코드를 작성할 수 있도록 돕는 강력한 도구입니다.
이번 핸드북에서는 이터레이터와 제너레이터가 왜 자바스크립트에 등장하게 되었는지 그 배경부터 시작하여, 내부 구조와 작동 원리, 그리고 실전에서 어떻게 활용할 수 있는지에 대한 심화 내용까지 깊이 있게 파헤쳐 보겠습니다.
1. 왜 필요했을까? 이터레이터와 제너레이터의 탄생 배경
초기 자바스크립트에서 여러 데이터를 순회하는 가장 일반적인 방법은 for 루프를 사용하는 것이었습니다.
JavaScript
const arr = [1, 2, 3];
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
이 방식은 간단하고 직관적이지만, 몇 가지 본질적인 문제점을 안고 있었습니다.
-
다양성의 부재:
for루프는 배열처럼 인덱스와length속성을 가진 자료 구조에 최적화되어 있습니다. 만약Map이나Set처럼 인덱스가 없는 자료 구조를 순회하려면 다른 방법을 사용해야 했죠. 이는 데이터 구조의 종류에 따라 순회 코드가 달라져야 함을 의미했고, 코드의 일관성을 해쳤습니다. -
내부 구현의 노출:
for루프를 사용하려면 배열의length속성과 각 요소의 인덱스에 직접 접근해야 합니다. 이는 데이터 구조의 내부 구현이 코드에 그대로 노출되는 것을 의미하며, 캡슐화 원칙에 위배될 수 있습니다. -
유연하지 않은 순회 제어:
for루프는 기본적으로 처음부터 끝까지 순차적으로 진행됩니다. 물론break나continue를 사용하여 제어할 수는 있지만, 순회를 잠시 멈추었다가 나중에 다시 시작하는 것과 같은 복잡한 제어는 구현하기 까다로웠습니다.
이러한 문제들을 해결하기 위해 ES6(ECMAScript 2015)에서 **이터레이션 프로토콜(Iteration Protocol)**이라는 새로운 표준이 도입되었습니다. 이 프로토콜은 “순회 가능한(iterable)” 데이터와 “순회하는 도구(iterator)“를 명확히 구분하고, 이 둘 사이의 상호작용 방식을 정의합니다. 그리고 이 프로토콜을 가장 쉽게 구현하고 활용할 수 있도록 도와주는 특별한 함수가 바로 제너레이터입니다.
비유하자면, 다양한 종류의 음악 앨범(데이터 컬렉션)이 있다고 상상해 보세요. 이전에는 각 앨범마다 다른 재생 기기(순회 방식)가 필요했다면, 이터레이션 프로토콜은 모든 앨범에 ‘재생’ 버튼과 ‘다음 곡’ 버튼이라는 표준 인터페이스를 만들어 준 것과 같습니다. 이제 우리는 어떤 앨범이든 동일한 리모컨(이터레이터)으로 제어할 수 있게 된 것입니다.
2. 내부를 들여다보자 이터레이터의 구조와 원리
이터레이션 프로토콜은 두 가지 핵심 요소로 구성됩니다. 바로 **이터러블 프로토콜(Iterable Protocol)**과 **이터레이터 프로토콜(Iterator Protocol)**입니다.
가. 이터러블 프로토콜 순회 가능한 자격 증명
어떤 객체가 Symbol.iterator라는 특별한 심볼(Symbol) 속성을 가지고 있고, 이 속성이 이터레이터 객체를 반환하는 함수라면, 그 객체는 **이터러블(iterable)**하다고 말합니다. 즉, ‘순회 가능한 자격’을 갖추었다는 의미입니다.
자바스크립트의 내장 객체인 Array, String, Map, Set 등은 모두 기본적으로 이터러블합니다. 그래서 우리는 for...of 문법을 사용하여 이들을 자연스럽게 순회할 수 있는 것입니다.
for...of 루프는 내부적으로 순회 대상 객체의 Symbol.iterator 메서드를 호출하여 이터레이터 객체를 얻은 다음, 이터레이터의 next() 메서드를 반복적으로 호출하며 순회를 진행합니다.
나. 이터레이터 프로토콜 실제 순회를 담당하는 일꾼
**이터레이터(Iterator)**는 순회의 실질적인 주체입니다. 이터레이터는 반드시 next()라는 이름의 메서드를 가져야 하며, 이 next() 메서드는 두 개의 속성을 가진 객체를 반환해야 합니다.
-
value: 현재 순회 단계의 값입니다. -
done: 순회가 모두 끝났는지를 나타내는 불리언(boolean) 값입니다. 순회가 진행 중이면false, 끝났으면true가 됩니다.
말로만 들으면 조금 복잡하게 느껴질 수 있으니, 간단한 배열을 통해 이터레이터가 어떻게 동작하는지 직접 살펴보겠습니다.
JavaScript
const arr = ['a', 'b', 'c'];
// 1. 배열의 Symbol.iterator 메서드를 호출하여 이터레이터 객체를 얻는다.
const iterator = arr[Symbol.iterator]();
// 2. 이터레이터의 next() 메서드를 호출하여 순회를 진행한다.
console.log(iterator.next()); // { value: 'a', done: false }
console.log(iterator.next()); // { value: 'b', done: false }
console.log(iterator.next()); // { value: 'c', done: false }
console.log(iterator.next()); // { value: undefined, done: true }
next()를 호출할 때마다 이터레이터는 다음 요소를 value에 담아 반환하고, done은 false로 유지합니다. 모든 요소를 다 순회한 후 다시 next()를 호출하면, value는 undefined가 되고 마침내 done이 true가 되면서 순회가 종료되었음을 알립니다. for...of 루프는 바로 이 done이 true가 될 때까지 내부적으로 next()를 계속 호출하는 방식으로 동작하는 것입니다.
이처럼 이터레이터는 상태를 가지는 객체입니다. 자신이 어디까지 순회했는지를 기억하고 있다가 next() 호출이 들어오면 다음 요소를 반환해 줍니다. 이러한 방식 덕분에 순회를 잠시 멈추거나, 필요할 때만 데이터를 하나씩 꺼내 쓰는 **지연 평가(Lazy Evaluation)**가 가능해집니다.
3. 더 쉽고 우아하게 제너레이터 사용법
이터레이션 프로토콜을 직접 구현하는 것은 가능하지만, next() 메서드 안에서 현재 순회 상태를 계속 추적하고 관리해야 하므로 다소 번거롭고 코드가 길어질 수 있습니다. 제너레이터는 바로 이러한 불편함을 해결하기 위해 등장한 문법적 설탕(Syntactic Sugar)입니다.
제너레이터는 이터레이터 객체를 손쉽게 생성할 수 있는 특별한 종류의 함수입니다. 일반 함수와 구분하기 위해 function 키워드 뒤에 별표(*)를 붙여 선언합니다.
JavaScript
function* myGenerator() {
// ...
}
제너레이터 함수의 가장 큰 특징은 yield라는 키워드를 사용할 수 있다는 점입니다. yield는 제너레이터 함수의 실행을 일시적으로 멈추고, yield 뒤에 오는 값을 이터레이터의 value로 반환합니다. 그리고 다음 next()가 호출될 때까지 멈춘 지점에서 대기합니다.
return이 함수를 완전히 종료시키는 반면, yield는 잠시 멈추고 밖으로 나갔다가 다시 돌아올 수 있는 ‘중간 정류장’과 같은 역할을 하는 셈입니다.
가. 제너레이터 기본 사용법
앞서 이터레이터를 직접 만들었던 예제를 제너레이터로 바꾸면 코드가 얼마나 간결해지는지 확인해 보겠습니다.
JavaScript
function* createArrayIterator(arr) {
for (const item of arr) {
yield item;
}
}
const arr = ['a', 'b', 'c'];
const iterator = createArrayIterator(arr); // 제너레이터 함수를 호출하면 이터레이터가 반환된다.
console.log(iterator.next()); // { value: 'a', done: false }
console.log(iterator.next()); // { value: 'b', done: false }
console.log(iterator.next()); // { value: 'c', done: false }
console.log(iterator.next()); // { value: undefined, done: true }
function*와 yield를 사용했을 뿐인데, 상태를 관리하는 별도의 변수나 복잡한 로직 없이도 완벽하게 동일한 동작을 하는 이터레이터를 만들어냈습니다. 제너레이터 함수를 호출하면 함수 본문이 바로 실행되는 것이 아니라, 이터레이터 객체가 먼저 생성되어 반환됩니다. 그리고 next() 메서드가 호출될 때마다 yield를 만날 때까지 코드가 실행되고 멈추기를 반복하는 것입니다.
나. yield*를 이용한 위임
제너레이터 함수 안에서 다른 이터러블이나 제너레이터를 호출하고 그 순회 결과를 현재 제너레이터의 순회 결과에 포함시키고 싶을 때 yield* 표현식을 사용할 수 있습니다. 이는 마치 순회의 제어권을 다른 이터러블에게 잠시 ‘위임’하는 것과 같습니다.
JavaScript
function* generator1() {
yield 1;
yield 2;
}
function* generator2() {
yield* generator1(); // generator1의 모든 yield 값을 순서대로 반환
yield 3;
}
const iter = generator2();
console.log(iter.next()); // { value: 1, done: false }
console.log(iter.next()); // { value: 2, done: false }
console.log(iter.next()); // { value: 3, done: false }
yield*는 복잡한 순회 로직을 여러 개의 작은 제너레이터 함수로 분리하여 코드의 재사용성과 가독성을 높이는 데 유용하게 사용됩니다.
4. 실전 활용과 심화 내용
이터레이터와 제너레이터는 단순히 for...of 루프를 지원하는 것을 넘어, 자바스크립트 프로그래밍의 패러다임을 바꿀 수 있는 강력한 잠재력을 가지고 있습니다.
가. 무한 데이터 스트림 구현하기
제너레이터는 ‘필요할 때 값을 계산해서 반환’하는 지연 평가의 특성을 가지고 있기 때문에, 이론적으로 무한한 데이터 스트림을 표현하는 데 매우 효과적입니다. 예를 들어, 무한히 증가하는 숫자 시퀀스를 만드는 제너레이터를 작성할 수 있습니다.
JavaScript
function* infiniteSequence() {
let num = 0;
while (true) {
yield num++;
}
}
const iterator = infiniteSequence();
console.log(iterator.next().value); // 0
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
// ... 무한히 계속됨
이 코드는 while(true) 루프를 사용하지만 프로그램이 멈추지 않습니다. 왜냐하면 yield를 만날 때마다 함수의 실행이 멈추고, 다음 next() 호출이 있을 때만 루프가 한 단계 더 진행되기 때문입니다. 이러한 특성은 대용량 데이터를 한 번에 메모리에 올리지 않고 필요한 만큼씩 처리해야 할 때 유용하게 사용될 수 있습니다.
나. 비동기 처리의 혁신 async/await의 조상
오늘날 자바스크립트 비동기 처리의 표준으로 자리 잡은 async/await 문법은 사실 제너레이터의 개념에서 직접적인 영감을 받아 탄생했습니다. async/await가 등장하기 전, 개발자들은 제너레이터와 Promise를 결합하여 비동기 코드를 동기식 코드처럼 보이게 만드는 라이브러리(예: co.js)를 사용하곤 했습니다.
제너레이터의 yield가 코드 실행을 멈출 수 있다는 점을 이용한 것입니다.
-
비동기 작업(Promise)을
yield합니다. -
제너레이터 실행기(Runner)가 Promise가 완료될 때까지 기다립니다.
-
Promise가 완료되면 그 결과값을
next()메서드에 인자로 전달하여 제너레이터의 실행을 재개시킵니다.
JavaScript
// async/await의 원리를 보여주는 개념적인 코드
function* asyncTask() {
const result1 = yield fetch('url1'); // 멈춤
const data1 = yield result1.json(); // 멈춤
console.log(data1);
const result2 = yield fetch('url2'); // 멈춤
const data2 = yield result2.json(); // 멈춤
console.log(data2);
}
// 실제로는 이 과정을 처리해주는 실행기(runner) 함수가 필요합니다.
async 함수는 내부적으로 제너레이터처럼 동작하며, await 키워드는 yield처럼 Promise가 완료될 때까지 실행을 멈추는 역할을 합니다. 이처럼 제너레이터는 현대 자바스크립트 비동기 프로그래밍의 근간을 이루는 매우 중요한 개념입니다.
다. 제너레이터와 외부의 양방향 통신
제너레이터의 next() 메서드는 인자를 받을 수 있습니다. 이 인자는 제너레이터 함수 내부에서 yield 표현식의 반환값이 됩니다. 이를 통해 제너레이터 외부에서 내부로 데이터를 주입하여 제너레이터의 동작을 제어하는, 즉 양방향 통신이 가능해집니다.
JavaScript
function* calculator() {
let result = 0;
while (true) {
const input = yield result; // 외부에서 들어온 값을 input에 할당
if (input.op === 'add') {
result += input.val;
} else if (input.op === 'sub') {
result -= input.val;
}
}
}
const calc = calculator();
calc.next(); // 첫 호출은 제너레이터를 시작시키는 역할
console.log(calc.next({ op: 'add', val: 10 }).value); // 10
console.log(calc.next({ op: 'add', val: 5 }).value); // 15
console.log(calc.next({ op: 'sub', val: 3 }).value); // 12
이처럼 제너레이터를 일종의 상태 머신(State Machine)처럼 활용하여 복잡한 상호작용 로직을 명확하고 구조적으로 구현할 수 있습니다.
5. 마무리하며
이터레이터와 제너레이터는 처음 접했을 때 다소 생소하고 복잡하게 느껴질 수 있습니다. 하지만 그 핵심은 ‘순회’라는 공통 작업을 표준화하고, 코드의 실행 흐름을 유연하게 제어하는 능력에 있습니다.
단순히 배열을 순회하는 것을 넘어, 무한한 데이터 스트림을 다루고, 복잡한 비동기 로직을 우아하게 처리하며, 데이터 생산자와 소비자 간의 상호작용을 구현하는 등 그 활용 범위는 무궁무진합니다. 이들은 자바스크립트를 한 단계 더 깊이 이해하고, 더 높은 수준의 추상화를 통해 효율적이고 가독성 높은 코드를 작성하기 위한 필수적인 도구입니다.
오늘 이 핸드북을 통해 이터레이터와 제너레이터의 작동 원리를 이해하고, 여러분의 코드에 직접 적용해 보세요. 아마도 이전에는 복잡하게만 보였던 문제들을 훨씬 더 간결하고 명확하게 해결할 수 있는 새로운 길을 발견하게 될 것입니다.