JavaScript[generator] - 제너레이터와 비동기 제너레이터
소개: 왜 제너레이터를 알아야 할까요?
제너레이터는 단순한 함수가 아닙니다. 실행을 일시 중지했다가 재개할 수 있는 특별한 함수로, 복잡한 비동기 흐름 제어부터 메모리 효율적인 데이터 처리까지 다양한 상황에서 강력한 솔루션을 제공합니다.
이 글에서는 제너레이터의 기본 개념부터 실제 프로덕션 환경에서의 활용 패턴까지 실용적인 예제와 함께 살펴보겠습니다.
목차
- 제너레이터 기본 개념
- 제너레이터 활용 패턴
- 비동기 제너레이터 이해하기
- 실무 활용 사례
- 성능 고려사항과 최적화
- 결론
제너레이터 기본 개념
제너레이터란 무엇인가?
제너레이터는 함수의 실행을 중간에 멈추고 재개할 수 있는 특별한 함수입니다. 일반 함수와는 달리, 제너레이터는 yield 키워드를 사용해 값을 하나씩 반환하고 다음 호출 시 중단된 지점부터 다시 실행을 계속합니다.
// 기본적인 제너레이터 함수 선언
function* simpleGenerator() {
console.log('시작');
yield 1; // 첫 번째 실행 중지 지점
console.log('중간');
yield 2; // 두 번째 실행 중지 지점
console.log('끝');
return 3; // 제너레이터 종료 및 반환값
}
// 제너레이터 객체 생성
const generator = simpleGenerator();
// next() 메서드로 실행 제어
console.log(generator.next()); // { value: 1, done: false } 출력 및 '시작' 로그
console.log(generator.next()); // { value: 2, done: false } 출력 및 '중간' 로그
console.log(generator.next()); // { value: 3, done: true } 출력 및 '끝' 로그
console.log(generator.next()); // { value: undefined, done: true }
제너레이터의 핵심 특징
- 실행 중지 및 재개: yield 키워드를 만나면 실행이 중지되고, next() 호출 시 재개됩니다.
- 상태 유지: 지역 변수와 실행 컨텍스트가 다음 호출까지 보존됩니다.
- 이터러블 프로토콜 구현: 제너레이터는 이터러블 객체를 반환하므로 for...of 루프 등에서 사용 가능합니다.
- 양방향 통신: next(value) 메서드를 통해 제너레이터에 값을 전달할 수 있습니다.
제너레이터 활용 패턴
1. 지연 평가 (Lazy Evaluation)
큰 데이터셋을 다룰 때 메모리 효율성을 높이는 패턴입니다.
// 무한 시퀀스 구현 (TypeScript)
function* infiniteSequence(): Generator<number, void, undefined> {
let i = 0;
while (true) {
yield i++;
// 무한 루프지만 yield로 실행이 중단되므로
// 실제로는 next()가 호출될 때만 다음 값이 계산됨
}
}
// 사용 예
const numbers = infiniteSequence();
console.log(numbers.next().value); // 0
console.log(numbers.next().value); // 1
console.log(numbers.next().value); // 2
// 처음 10개 숫자만 처리하기
let count = 0;
for (const num of infiniteSequence()) {
if (count >= 10) break;
console.log(num);
count++;
}
2. 커스텀 이터러블 객체 구현
컬렉션 타입을 쉽게 순회 가능하게 만드는 패턴입니다.
// 커스텀 범위 이터레이터 (TypeScript)
function* range(start: number, end: number, step: number = 1): Generator<number, void, undefined> {
for (let i = start; i <= end; i += step) {
yield i;
}
}
// 사용 예
for (const num of range(1, 10, 2)) {
console.log(num); // 1, 3, 5, 7, 9
}
// 배열로 변환
const rangeArray = [...range(1, 5)]; // [1, 2, 3, 4, 5]
3. 제너레이터 위임 (Delegation)
복잡한 제너레이터 조합을 간결하게 표현할 수 있는 패턴입니다.
function* generateAlphabets() {
yield 'a';
yield 'b';
yield 'c';
}
function* generateNumbers() {
yield 1;
yield 2;
yield 3;
}
function* combined() {
// yield* 표현식은 다른 제너레이터에 위임
yield* generateAlphabets();
yield* generateNumbers();
}
const iterator = combined();
for (const value of iterator) {
console.log(value); // 'a', 'b', 'c', 1, 2, 3 순서대로 출력
}
비동기 제너레이터 이해하기
ES2018에서 도입된 비동기 제너레이터는 비동기 작업과 제너레이터의 강력한 결합입니다.
기본 문법과 작동 방식
// 비동기 제너레이터 함수 선언
async function* asyncGenerator() {
yield Promise.resolve(1);
await new Promise(resolve => setTimeout(resolve, 1000));
yield 2;
yield Promise.resolve(3);
}
// 비동기 제너레이터 사용하기
(async () => {
const generator = asyncGenerator();
// for await...of 루프로 간편하게 소비
for await (const value of generator) {
console.log(value); // 1, 2, 3이 순차적으로 출력됨 (1초 지연 포함)
}
// 또는 next() 메서드를 수동으로 호출
// const result1 = await generator.next();
// console.log(result1.value); // 1
})();
스트림 처리와의 연계
비동기 제너레이터는 Node.js의 스트림이나 웹의 Fetch API 등과 연계하여 메모리 효율적인 데이터 처리를 구현할 수 있습니다.
// 웹 API를 통한 데이터 스트리밍 예제
async function* fetchCommitsAsStream(repo) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`https://api.github.com/repos/${repo}/commits?page=${page}`);
const commits = await response.json();
if (commits.length === 0) {
hasMore = false;
} else {
for (const commit of commits) {
yield {
sha: commit.sha,
message: commit.commit.message,
author: commit.commit.author.name
};
}
page++;
}
}
}
// 사용 예
(async () => {
const commits = fetchCommitsAsStream('facebook/react');
// 최대 10개 커밋만 처리
let count = 0;
for await (const commit of commits) {
console.log(`${commit.sha.substring(0, 7)} - ${commit.message}`);
if (++count >= 10) break;
}
})();
실무 활용 사례
무한스크롤 처리
// 서버에서 페이지네이션된 데이터를 가져오는 함수
async function fetchPageData(page: number): Promise<{
items: Product[],
hasMore: boolean
}> {
const response = await fetch(`/api/products?page=${page}&limit=20`);
return response.json();
}
// 비동기 제너레이터를 사용한 무한 스크롤 데이터 로더
async function* createInfiniteLoader<T>() {
let page = 1;
let hasMore = true;
while (hasMore) {
const { items, hasMore: moreAvailable } = await fetchPageData(page);
// 받아온 항목들을 하나씩 yield
for (const item of items) {
yield item;
}
hasMore = moreAvailable;
page++;
}
}
// React 컴포넌트에서 사용 예시 (간략화)
function ProductList() {
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(false);
const loaderRef = useRef<HTMLDivElement>(null);
const productsGenerator = useRef<AsyncGenerator<Product> | null>(null);
// 컴포넌트 마운트 시 제너레이터 초기화 및 Intersection Observer 설정
useEffect(() => {
productsGenerator.current = createInfiniteLoader<Product>();
loadMoreItems();
const observer = new IntersectionObserver(
entries => {
// 관찰 대상이 화면에 보이면 추가 아이템 로드
if (entries[0].isIntersecting && !loading) {
loadMoreItems();
}
},
{ rootMargin: "100px" }
);
if (loaderRef.current) {
observer.observe(loaderRef.current);
}
return () => observer.disconnect();
}, []);
// 추가 아이템 로드 함수
const loadMoreItems = async () => {
if (!productsGenerator.current || loading) return;
setLoading(true);
const newProducts: Product[] = [];
try {
// 한 번에 20개 항목만 로드
for (let i = 0; i < 20; i++) {
const result = await productsGenerator.current.next();
if (result.done) break;
newProducts.push(result.value);
}
setProducts(prev => [...prev, ...newProducts]);
} finally {
setLoading(false);
}
};
return (
<div>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
{/* 이 div가 화면에 보이면 추가 로드 발생 */}
<div ref={loaderRef} />
{loading && <div>Loading...</div>}
</div>
);
}
- 비동기 제너레이터(createInfiniteLoader):
- 페이지 번호를 내부적으로 관리
- 각 페이지의 데이터를 가져와서 한 항목씩 yield
- 모든 데이터를 한 번에 메모리에 로드하지 않고 필요할 때만 가져옴
- Intersection Observer:
- 특정 요소(loaderRef)가 화면에 보이는지 감지
- 요소가 화면에 보이면 추가 데이터 로드 트리거
- 제너레이터 상태 관리:
- 제너레이터는 마지막으로 yield한 지점을 기억하고 있음
- 다음 next() 호출 시 중단된 지점부터 실행 재개
- 이전에 로드한 페이지를 다시 요청하지 않음
이 패턴을 사용하면 사용자가 스크롤을 내릴 때 자연스럽게 추가 데이터가 로드되어 무한 스크롤을 구현할 수 있습니다. 제너레이터가 내부적으로 페이지 상태를 관리하므로 컴포넌트에서 별도로 페이지 번호를 관리할 필요가 없습니다.
성능 고려사항과 최적화
메모리 사용량
제너레이터의 가장 큰 장점 중 하나는 메모리 효율성입니다. 전체 데이터셋을 한 번에 메모리에 로드하는 대신, 필요한 항목만 생성하고 처리할 수 있습니다.
// 메모리 사용 비교
// 일반 배열 접근법 - 모든 데이터를 한 번에 메모리에 로드
function getMillionNumbers() {
const numbers = [];
for (let i = 0; i < 1000000; i++) {
numbers.push(i);
}
return numbers;
}
// 제너레이터 접근법 - 필요할 때만 값을 생성
function* generateMillionNumbers() {
for (let i = 0; i < 1000000; i++) {
yield i;
}
}
// 성능 테스트
console.time('Array Method');
const arr = getMillionNumbers();
let sum1 = 0;
for (let i = 0; i < 1000; i++) { // 처음 1000개만 사용
sum1 += arr[i];
}
console.timeEnd('Array Method');
console.time('Generator Method');
const gen = generateMillionNumbers();
let sum2 = 0;
for (let i = 0; i < 1000; i++) {
sum2 += gen.next().value;
}
console.timeEnd('Generator Method');
트러블슈팅: 일반적인 문제들
제너레이터 상태 재설정
제너레이터는 한 번 소비되면 재사용할 수 없습니다. 이유는 다음과 같습니다:
- 내부 상태 유지: 제너레이터는 실행 상태를 내부적으로 유지합니다. 값을 yield할 때마다 그 지점을 기억하고, 다음 next() 호출 시 정확히 그 지점부터 실행을 재개합니다.
- 일회성 이터레이터: 제너레이터 함수가 반환하는 제너레이터 객체는 일회성 이터레이터입니다. 모든 값을 소비하면(또는 중간에 중단되더라도) 그 이터레이터는 완료된 상태가 됩니다.
// 문제: 제너레이터를 재사용할 수 없음
const numbers = generateNumbers(); // 제너레이터 객체 생성
for (const n of numbers) { // 이 루프에서 값 소비
console.log(n);
if (n > 5) break; // 6까지 출력 후 중단
}
// 이미 소비되었으므로 아무것도 출력하지 않음
for (const n of numbers) {
console.log('다시:', n); // 실행되지 않음 - 제너레이터가 이미 소비됨
}
// 해결책: 제너레이터 함수를 팩토리로 사용
function* generateNumbers() {
let i = 0;
while (i < 10) yield i++;
}
// 새 제너레이터 인스턴스 생성
// 첫 번째 루프에서 직접 제너레이터 함수를 호출하여 새 인스턴스 생성
for (const n of generateNumbers()) {
console.log(n);
if (n > 5) break;
}
// 다시 새 인스턴스 생성
// 두 번째 루프에서도 다시 제너레이터 함수를 호출하여 새 인스턴스 생성
for (const n of generateNumbers()) {
console.log('다시:', n); // 정상 작동
}
왜 팩토리 패턴이 가능한가?
이 방식이 가능한 이유는:
- 함수 재실행: generateNumbers()를 호출할 때마다 함수가 처음부터 다시 실행됩니다.
- 초기 상태 재설정: 매번 let i = 0으로 카운터를 초기화합니다.
- 새 제너레이터 객체: 함수 호출은 매번 완전히 새로운 제너레이터 객체를 반환합니다.
비동기 제너레이터와 일반 제너레이터 혼합
// 안티패턴: 종료 조건 없는 무한 제너레이터를 위험하게 사용
function* infiniteGenerator() {
let i = 0;
while (true) {
yield i++;
}
}
// 위험: 무한 루프
const values = [...infiniteGenerator()]; // 메모리 초과 오류 발생
// 올바른 사용법: 명시적 제한
const gen = infiniteGenerator();
const firstTen = Array.from({ length: 10 }, () => gen.next().value);
전문 용어 설명
- 이터러블(Iterable): Symbol.iterator 메서드를 구현한 객체입니다. 이 메서드는 이터레이터를 반환하며, for...of 루프, 스프레드 연산자(...), 구조 분해 할당 등에서 사용할 수 있습니다. 배열, 문자열, Map, Set 등이 기본적인 이터러블입니다.
- 이터레이터(Iterator): next() 메서드를 가진 객체로, 호출될 때마다 { value, done } 형태의 객체를 반환합니다. value는 현재 값이고, done은 시퀀스가 끝났는지를 나타내는 불리언 값입니다.
- 제너레이터 함수(Generator Function): function* 문법으로 선언되는 특별한 함수입니다. 일반 함수와 달리 실행을 중간에 중지하고 재개할 수 있으며, 호출 시 제너레이터 객체(이터레이터이자 이터러블)를 반환합니다.
- yield: 제너레이터 함수 내에서 값을 반환하면서 실행을 일시 중지시키는 키워드입니다. 제너레이터의 next()가 다시 호출될 때 중단된 지점에서 실행이 재개됩니다.
- yield*: 다른 제너레이터나 이터러블에 제어를 위임하는 표현식입니다. 이를 통해 한 제너레이터가 다른 제너레이터나 이터러블의 모든 값을 순차적으로 yield할 수 있습니다.
function* example1() {
yield [1, 2, 3]; // [1, 2, 3] 배열 자체를 하나의 값으로 반환
}
function* example2() {
yield* [1, 2, 3]; // 1, 2, 3을 각각 개별적으로 순차 반환
}
// 사용 예
for (const val of example1()) {
console.log(val); // [1, 2, 3] 한 번만 출력됨
}
for (const val of example2()) {
console.log(val); // 1, 2, 3이 각각 순서대로 출력됨
}
yield*는 이터러블 객체의 각 요소를 개별적으로 yield 하도록 제어를 위임하는 것입니다.
마치 다음과 같이 작성한 것과 동일한 효과입니다:
function* example2Manual() {
for (const item of [1, 2, 3]) {
yield item;
}
}
- 비동기 제너레이터(Async Generator): async function* 문법으로 선언된 함수로, 비동기 작업을 yield로 처리할 수 있습니다.
- for await...of: 비동기 이터러블을 순회하기 위한 루프 구문입니다.
- 팩토리로 사용한다 / 팩토리 패턴 : 제너레이터 함수 자체를 호출하여 매번 새로운 제너레이터 객체를 생성하는 것을 의미합니다. 이는 디자인 패턴에서의 팩토리 패턴과 유사합니다 - 팩토리는 새로운 객체 인스턴스를 생성하는 함수입니다.
- 컬렉션 타입 : 컬렉션 타입은 여러 데이터 요소들을 하나로 묶어서 관리하는 자료구조를 말합니다. JavaScript에서 기본적으로 제공하는 컬렉션 타입에는:
배열(Array)
객체(Object)
Map
Set
WeakMap
WeakSet
등이 있습니다. 이들은 여러 값을 담고 관리하는 자료구조입니다.
결론 및 더 공부할 자료
제너레이터
ko.javascript.info
Generator - JavaScript | MDN
Generator 객체는 generator function 으로부터 반환되며, 반복 가능한 프로토콜과 반복자 프로토콜을 모두 준수합니다.
developer.mozilla.org
제너레이터 자체를 많이 사용해보거나 접해보지 못해서 조금은 생소하지만 해당 내용을 찾아보면서 실무에 적절히 적용하면 더 나은 코드를 만들 수 있을 것 같다는 생각이 들었다.
'JavaScript' 카테고리의 다른 글
[JS] 웹 스토리지(LocalStoratge / Sesstion Storage) 활용 및 차이 feat.cookie (1) | 2025.04.16 |
---|---|
자바스크립트 엔진의 주요 구성 요소 (0) | 2025.03.31 |
CommonJS와 ES Modules: 자바스크립트 모듈 시스템 (0) | 2025.03.10 |
[javascript] - 자바스크립트 함수에 대해서 아는대로 설명해주세요. (1) | 2025.03.07 |
JavaScript 배열 기초 정리: 핵심 개념부터 유용한 메서드까지 (1) | 2025.03.06 |