
Iterator 공부하다가 Generator라는 걸 발견했는데... 처음엔 복잡해 보였지만 실제로 써보니 진짜 신기한 기능이더라. 특히 메모리 관리 측면에서 엄청난 차이가 났다.
// 일반 함수 - 모든 값을 한번에 만듦
function normalNumbers() {
const result = [];
for (let i = 0; i <= 1000; i++) {
result.push(i);
}
return result; // 메모리에 1001개 모두 저장
}
// Generator - 필요할 때만 하나씩 만듦
function* generatorNumbers() {
for (let i = 0; i <= 1000; i++) {
yield i; // 하나씩만 만들어서 반환
}
}
function* simpleGenerator(): Generator<number, void, unknown> {
yield 1;
yield 2;
yield 3;
}
const iter = simpleGenerator();
console.log(iter.next()); // { value: 1, done: false }
console.log(iter.next()); // { value: 2, done: false }
console.log(iter.next()); // { value: 3, done: false }
console.log(iter.next()); // { value: undefined, done: true }
핵심 발견: Generator는 필요할 때만 하나씩 값을 만들어준다
done: true가 되면 더 이상 값이 없음yield에서 일시정지했다가 next() 호출하면 재개// 10만개 데이터 중에서 처음 10개만 필요한 상황
function* processLargeData() {
for (let i = 0; i < 100000; i++) {
// 복잡한 계산이나 API 호출
const processed = heavyCalculation(i);
yield processed;
}
}
// 필요한 만큼만 처리하고 중단
let count = 0;
for (const data of processLargeData()) {
console.log(data);
if (++count >= 10) break; // 10개만 처리하고 끝
}
function* conditionalGenerator(max: number) {
for (let i = 1; i <= max; i++) {
if (i % 2 === 0) {
yield i; // 짝수만 반환
}
}
}
// 사용할 때도 깔끔하게
for (const evenNumber of conditionalGenerator(1000)) {
console.log(evenNumber); // 2, 4, 6, 8, 10...
}
Generator는 단순히 값을 뱉기만 하는 게 아니라 대화할 수 있다는 게 진짜 신기했다.
function* twoWayGenerator(): Generator<string, void, string> {
const input = yield "사자의 다리 갯수는?";
yield `받은 답변: ${input}`;
}
const gen = twoWayGenerator();
console.log(gen.next()); // { value: '사자의 다리 갯수는?', done: false }
console.log(gen.next("4개")); // { value: '받은 답변: 4개', done: false }
console.log(gen.next()); // { value: undefined, done: true }
Generator<YieldType, ReturnType, NextType>
YieldType: yield로 반환하는 값의 타입ReturnType: return으로 반환하는 값의 타입 NextType: next()로 전달받는 값의 타입핵심 포인트:
next()는 값을 전달하지 않음next(value)부터 값이 yield 표현식으로 전달됨Generator를 처음 쓸 때 당황했던 부분이다.
function* simpleGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = simpleGenerator();
// 첫 번째 사용
for (const value of gen) {
console.log(value); // 1, 2, 3 출력
}
// 두 번째 사용 (같은 gen)
for (const value of gen) {
console.log("두 번째:", value); // 아무것도 출력 안됨!
}
해결법: 새로운 Generator 객체를 생성하자
const gen2 = simpleGenerator();
for (const value of gen2) {
console.log("새로운 gen:", value); // 1, 2, 3 정상 출력
}
이게 진짜 Generator의 진가가 드러나는 부분이다.
function* naturals() {
let n = 1;
while (true) {
yield n++; // 무한히 자연수 생성
}
}
const gen = naturals();
let count = 0;
for (const natural of gen) {
console.log(natural); // 1, 2, 3, 4, 5
count++;
if (count >= 5) break; // 필요한 만큼만 사용
}
핵심:
while(true)로 무한 루프를 만들어도 괜찮다처음엔 이게 뭔지 몰랐는데, 알고 보니 위임하는 키워드였다.
function* test1() {
yield 1;
yield [2, 3]; // 배열 자체를 반환
yield 4;
}
// 결과: 1, [2, 3], 4
function* test2() {
yield 1;
yield* [2, 3]; // 배열을 펼쳐서 각각 반환
yield 4;
}
// 결과: 1, 2, 3, 4
차이점:
yield: 값 자체를 반환yield*: 이터러블을 펼쳐서 각각 반환 (위임)배열을 뒤집는 Generator를 만들어봤는데, 원본 배열을 건드리지 않아서 좋았다.
function* reverse<T>(array: T[]): Generator<T, void, unknown> {
let idx = array.length; // 3
while (--idx >= 0) { // 2, 1, 0 순서로 접근
yield array[idx];
}
}
const original = ["A", "B", "C"];
const reversed = reverse(original);
for (const item of reversed) {
console.log(item); // C, B, A 순서로 출력
}
console.log(original); // ["A", "B", "C"] - 원본 그대로!
idx = array.length (3)에서 시작--idx로 먼저 1을 빼고 사용: 2, 1, 0Generator를 쓸 때 컴파일 에러가 날 수 있다.
// tsconfig.json
{
"compilerOptions": {
"target": "ES2015", // 또는
"downlevelIteration": true // 이 옵션 추가
}
}
// 메모리 사용량 비교
const bigArray = Array.from({length: 1000000}, (_, i) => i);
// → 약 8MB 메모리 사용
function* bigGenerator() {
for (let i = 0; i < 1000000; i++) {
yield i;
}
}
// → 거의 0에 가까운 메모리 사용 (필요할 때만)
Generator는 처음엔 복잡해 보였지만, 실제로 써보니 메모리 효율성과 지연 계산의 강력함을 느낄 수 있었다.
다만 일회용이라는 특성과 작은 데이터에서는 오버엔지니어링이 될 수 있다는 점은 주의해야겠다.
다음엔 async Generator도 공부해봐야지... 비동기 데이터 스트리밍에 쓸 수 있을 것 같다!