[번역] TypeScript의 제너레이터 이해하기
— Translate, TypeScript — 16 min read

이 게시물은 원본 아티클인 Understanding TypeScript generators 를 한글로 번역한 게시글입니다. 게시물 내용의 저작권은 원작자 Debjyoti Banerjee 에게 있습니다.
일반적인 함수는 위에서부터 아래로 실행 된 후 종료됩니다. 제너레이터 함수도 위에서 아래로 실행되지만, 실행 도중 멈추고 나중에 그 부분에서 다시 시작할 수 있습니다. 이는 프로세스가 끝까지 진행되며, 그 후에 종료됩니다. 이번 아티클에서는 TypeScript에서 제너레이터 함수를 어떻게 사용하는지에 대해 다양한 예제와 사용 사례를 다뤄보겠습니다. 지금 바로 시작합니다!
TypeScript에서 제너레이터 함수 만들기
일반 함수는 즉시(eager)실행되는 반면에 제너레이터는 지연(lazy)실행으로 나중에 실행되도록 요청 할 수 있습니다.
제너레이터 함수를 만들기 위해서는 function *
커맨드를 사용합니다.
제너레이터 함수는 겉 보기에는 일반 함수 같지만 조금 다르게 행동합니다.
예제와 함께 알아보겠습니다.
function normalFunction() { console.log('This is a normal function');}
function* generatorFunction() { console.log('This is a generator function');}
normalFunction(); // "This is a normal function"generatorFunction();
일반함수와 작성하고 실행하는 것은 같아보이지만, generatorFunction
을 호출할 때 우리는 콘솔에서 어떠한 로그도 얻지 못합니다.
간단히 말해서, 제너레이터 함수를 호출하는 것은 코드를 실행하지 않습니다.

제너레이터 함수가 Generator
타입을 반환한다는 것을 알아채셨을 겁니다.
이는 다음 내용에서 더 자세히 알아보겠습니다.
제너레이터 함수를 실행하려면 다음과 같게 하면 됩니다.
function* generatorFunction() { console.log('This is a generator function');
const a = generatorFunction(); a.next();}
next
메소드가 IteratorResult
를 반환하는 것을 확인했습니다.
만약 generatorFunction
에서 숫자를 반환한다면, 우리는 그것을 이렇게 접근해야 합니다.
function* generatorFunction() { console.log('This is a generator function'); return 3;}const a = generatorFunction();const b = a.next();console.log(b); // {"value":3, "done":true}console.log(b.value); // 3
제너레이터 인터페이스는 Iterator
를 확장하며, 이를 통해 next
를 호출 할 수 있습니다.
또한 이는 [Symbol.iterator]
속성을 가지고 있어 반복 가능(iterable) 합니다.
JavaScript의 반복 가능한(iterable) 객체와 반복자(iterator) 이해하기
반복 가능한 객체는 for...of
문을 사용해 반복할 수 있는 객체 입니다.
이는 필수적으로 Symbol.iterator
메서드가 구현되어야 합니다.
예를 들어 JavaScript에서 배열은 반복 가능한(iterable) 객체이므로, 반드시 반복자(iterator)를 가지고 있어야 합니다.
const a = [1, 2, 3, 4];const it: Iterator<number> = a[Symbol.iterator]();
while (true) { let next = it.next(); if (!next.done) { console.log(next.value); } else { break; }}
반복자(iterator)는 반복 가능한 객체(iterable)를 반복할 수 있도록 만듭니다. 반복자를 구현하는 아주 간단한 코드를 보도록 하겠습니다.
function naturalNumbers() { let n = 0; return { next: function () { n += 1; return { value: n, done: false }; }, };}
const iterable = naturalNumbers();iterable.next().value; // 1iterable.next().value; // 2iterable.next().value; // 3iterable.next().value; // 4
앞서 말했듯이, 반복 가능한 객체는 Symbol.iterator
를 가지고 있습니다.
때문에 만약 위 예제 코드처럼 next()
함수를 반환하는 함수를 할당한다면, 해당 객체는 자바스크립트의 반복 가능한 객체가 됩니다.
이는 for..of
를 사용해서 반복이 가능합니다.
확실히, 지금까지 예제와 제너레이터 함수는 서로 비슷해 보입니다. 사실 제너레이터가 한번에 하나의 변수를 계산하기 때문에, 우리는 이를 이용해 쉽게 반복자를 구현할 수 있습니다.
TypeScript에서 제너레이터의 동작
제너레이터의 재미있는 것 중 하나는 이전 예제에서 사용하지 않았던 yield
문을 사용해 실행을 일시중지 할 수 있다는 것입니다.
next
가 호출되었을 때 제너레이터는 yield
까지 동기적으로 코드를 실행하고, 해당 부분에서 실행을 일시 중지합니다.
만약 next
가 다시 호출되면, 아까 일시 중지했던 부분부터 다시 실행을 시작합니다.
예제와 함께 확인해보겠습니다.
function* iterator() { yield 1; yield 2; yield 3;}for (let x of iterator()) { console.log(x);}
yield
는 기본적으로 함수에서 여러번 반환할 수 있게 해줍니다.
게다가 절대로 메모리에 배열을 생성하지 않아서, 매우 효율적인 방법으로 무한한 시퀀스를 생성할 수 있습니다.
무한 짝수 시퀀스를 만드는 예제를 한번 확인해보겠습니다.
function* evenNumbers(){ let n = 0; while(true){ yild n += 2; }}
const gen = evenNumbers();console.log(gen.next().value); // 2console.log(gen.next().value); // 4console.log(gen.next().value); // 6console.log(gen.next().value); // 8console.log(gen.next().value); // 10
또한 위 예제를 수정해서 파라미터로 숫자를 전달받고, 그 숫자부터 시작하도록 할 수 있습니다.
function* evenNumbers(start: number) { let n = start; while (true) { if (start === 0) { yield (n += 2); } else { yield n; n += 2; } }}const gen = evenNumbers(6);console.log(gen.next().value); // 6console.log(gen.next().value); // 8console.log(gen.next().value); // 10console.log(gen.next().value); // 12console.log(gen.next().value); // 14
TypeScript의 제너레이터 사용 예제
제너레이터는 데이터 흐름을 조절할 수 있는 강력한 메커니즘을 제공하고, TypeScript에서 유연하고 효율적이며 읽기 쉬운 코드를 만듭니다. 이는 필요에 따라 값을 생성하고, 비동기 작업을 처리하며, 사용자 정의 반복 로직을 생성할 수 있는 능력이 있어 몇 가지 상황에서 유융한 도구로 쓰입니다.
요청에 따라 값 계산
제너레이터를 구현하여 필요에 따라 값을 계산하고 반환할 수 있으며, 중간 결과 값을 캐싱해서 성능을 향상시킬 수 있습니다. 이 기술을 사용하면 비용이 많이 드는 계산이나, 특정 작업을 실행이 필요한 시점까지 지연하도록 할 때 유용합니다. 예제를 한번 보겠습니다.
while(true){ const next= prev + curr; yield next; prev = curr; curr = next;}
// (천천히) 피보나치 수열을 계산하기 위해 제너레이터 사용const fibonacciGenerator = calculateFibonacci();
// 피보나치 수열 처음 10개 계산for (let i = 0; i < 10; i++) { console.log(fibonacciGenerator.next().value); // 0, 1, 1, 2, 3, 5, 8, 13, 21, 24}
위 예제를 보면, 피보나치 수열을 앞에서 전부 계산하는 것이 아니라, 요청이 됐을 때 필요한 피보나치 숫자만 계산합니다. 이는 더 효율적으로 메모리를 사용하고, 필요한 시점에 값의 계산이 요청됩니다.
대용량 데이터 셋 반복
제너레이터는 대용량 데이터셋을 반복할 때 모든 데이터를 한번에 메모리에 로딩하지 않습니다. 그 대신, 필요할 때 값을 생성해서 메모리 효율성을 향상시킬 수 있습니다. 이건 특히 큰 데이터베이스나 파일들을 다룰 때 유용합니다.
function* iterateLargeData(): Generator<number> { const data = Array.from({ length: 1000000 }, (_, index) => index + 1);
for (const item of data) { yield item; }}
// 대용량 데이터셋을 반복하는 데 제너레이터 사용const dataGenerator = iterateLargeData();
for (const item of dataGenerator) { console.log(item); // 모든 데이터를 메모리에 로딩하지 않고 각 항목에 대해 작업을 수행}
예제를 보면, iterateLargeData
제너레이터 함수는 백만개의 숫자 배열로 구성 된 대용량 데이터 셋을 모사합니다.
전체 배열을 한번에 반환하는 대신에, yield
키워드를 사용 할 때마다 데이터를 생성합니다.
그러므로, 메모리에 모든 숫자를 로딩하지 않으면서 반복할 수 있습니다.
제너레이터 재귀적으로 사용하기
제너레이터의 메모리 효율적인 특성은 디렉토리 내부의 파일 이름을 재귀적으로 읽는 것 같은 작업에 더 유용하게 사용할 수 있습니다. 사실, 제너레이터를 생각할 때 저에게 자연스럽게 떠오르는 것은 중첩된 구조를 재귀적으로 탐색하는 것입니다.
yield
가 표현식 인 경우, yield*
를 사용해 다른 반복 가능한 객체에 위임할 수 있습니다.
예제를 보도록 하겠습니다.
function* readFilesRecursive(dir: string): Generator<string> { const files = fs.readdirSync(dir, { withFileTypes: true });
for (const file of files) { if (file.isDirectory()) { yield* readFilesRecursive(path.join(dir, file.name)); } else { yield path.join(dir, file.name); } }}
이 함수는 아래와 같이 사용할 수 있습니다.
for (const file of readFilesRecursive('/path/to/directory')) { console.log(file);}
또한 yield
를 사용해 제너레이터에 값을 전달 할 수 있습니다.
예제를 한번 보겠습니다.
function* sumNaturalNumbers(): Generator<number, any, number> { let value = 1; while (true) { const input = yield value; value += input; }}
const it = sumNaturalNumbers();it.next();console.log(it.next(2).value); // 3console.log(it.next(3).value); // 6console.log(it.next(4).value); // 10console.log(it.next(5).value); // 15
next(2)
가 호출됐을 때, input
에는 2
가 할당됩니다.
이와 마찬가지로 next(3)
이 호출되면, input
에는 3
이 할당됩니다.
에러 처리 (Error handling)
예외 처리와 실행 흐름을 제어하는 것은 제너레이터를 사용하고자 할 때 중요한 개념입니다. 제너레이터는 기본적으로 일반 함수처럼 보이고 구문도 동일합니다.
제너레이터가 에러를 만났을 때, throw
키워드를 사용해 예외를 발생시킬 수 있습니다.
예외는 제너레이터 함수 내부나 제너레이터를 사용하는 함수 바깥에서도 try...catch
블록으로 잡고 처리할 수 있습니다.
function* generateValues(): Generator<number, void, string> { try { yield 1; yield 2; throw new Error('Something went wrong'); yield 3; // 여긴 닿지 않을겁니다. } catch (error) { yield* handleError(error); // 에러를 처리하고 계속합니다. }}
function* handleError(error: Error): Generator<number, void, string> { yield 0; // 기본 값으로 계속합니다. yield* generateFallbackValues(); // 대체값 반환 throw `Error handled: ${error.message}`; // 새로운 에러 또는 기존 에러를 발생시킵니다.}
예시에서 제너레이터 함수 generateValues
는 2
를 반환한 후 에러를 발생시킵니다.
제너레이터 내부의 catch 구역에서 에러를 잡고, 다른 제너레이터 함수 handleError
로 제어권을 넘겨 대체값을 반환하고자 합니다.
마지막으로 제너레이터 함수 handleError
는 새로운 에러나 기존 에러를 발생시킵니다.
제너레이터를 사용 할 때 아래 처럼 try...catch
를 사용해 발생한 에러를 잡을 수 있습니다.
const generator = generateValues();
try { console.log(generator.next()); console.log(generator.next()); console.log(generator.next());} catch (error) { console.error('Caugh error:', error);}
이 경우에서는 에러가 catch 구역에서 잡히고, 그에 따라 적절히 처리할 수 있습니다.
결론
이번 아티클에서는 TypeScript에서 제너레이터를 사용하는 방법을 배웠으며, JavaScript의 반복자와 반복 가능한 객체의 기초를 복습했습니다. 또한 TypeScript에서 제너레이터를 재귀적으로 사용하는 방법과 에러를 처리하는 방법도 배웠습니다.
제너레이터는 유니크 ID를 생성하거나, 소수를 만들거나, 스트림 기반의 알고리즘을 구현하는 등 정말 많은 흥미로운 목적으로 사용할 수 있습니다. 조건을 사용하거나 제너레이터를 수동으로 중단시켜 시퀀스의 종료를 제어할 수 있습니다.
역주 의견
여기서부터는 저(himprover)의 의견 입니다.
하루에 한개씩 번역 아티클을 올려보자고 시작한지 이제 9일째 되었습니다.
제가 번역했던 아티클 중 가장 길이가 길지 않았나 싶습니다.
그래도 9일 동안 진행하며 번역 속도가 많이 나아진게 느껴집니다.
아티클의 내용 또한 흥미로워서 더 수월하게 진행한 것 같습니다.