JS-제너레이터

topics 200-프론트개발
types 이론 학습
tags
references ko.javascript.info/generators

제너레이터는 왜 쓸까

일반 함수는 하나의 값만 return한다. 근데 제너레이터는 여러 개의 값을 하나씩 반환(yield)할 수 있다.

기본 사용법

function* generateSequence() {
  yield 1;
  yield 2;
  yield 3;
}

const generator = generateSequence();

console.log(generator.next()); // {value: 1, done: false}
console.log(generator.next()); // {value: 2, done: false}
console.log(generator.next()); // {value: 3, done: false}
console.log(generator.next()); // {value: undefined, done: true}

function* 문법으로 정의하고, yield로 값을 하나씩 반환한다.

언제 쓸까

1. 무한 시퀀스 생성

배열로 만들면 메모리가 터지는데, 제너레이터는 필요할 때만 값을 생성한다.

function* infiniteSequence() {
  let num = 0;
  while (true) {
    yield num++;
  }
}

const sequence = infiniteSequence();
console.log(sequence.next().value); // 0
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
// 무한히 계속...

2. 이터러블 객체 생성

for...of로 순회 가능한 객체를 쉽게 만들 수 있다.

function* range(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}

for (let num of range(1, 5)) {
  console.log(num); // 1, 2, 3, 4, 5
}

3. 비동기 처리 (과거에)

요즘은 async/await을 쓰지만, 과거에는 제너레이터로 비동기를 동기처럼 작성했다.

function* fetchData() {
  const data = yield fetch('https://api.example.com/data');
  console.log(data);
}

const gen = fetchData();
const result = gen.next();

result.value
  .then(response => response.json())
  .then(data => gen.next(data));

지금은: async/await을 사용하자. 제너레이터로 비동기 처리하는 건 복잡하다.

제너레이터 컴포지션

제너레이터 안에서 다른 제너레이터를 호출할 수 있다.

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) yield i;
}

function* generatePasswordCodes() {
  yield* generateSequence(48, 57);   // 0..9
  yield* generateSequence(65, 90);   // A..Z
  yield* generateSequence(97, 122);  // a..z
}

console.log([...generatePasswordCodes()]);
// [48, 49, 50, ..., 122] - 총 62개

yield*로 다른 제너레이터에게 실행을 위임한다.

양방향 통신

제너레이터는 값을 받을 수도 있다!

function* gen() {
  let ask1 = yield "2 + 2 = ?";
  console.log(ask1); // 4

  let ask2 = yield "3 * 3 = ?";
  console.log(ask2); // 9
}

const generator = gen();

console.log(generator.next().value);    // "2 + 2 = ?"
console.log(generator.next(4).value);   // "3 * 3 = ?"
console.log(generator.next(9).done);    // true

next(value)로 값을 전달하면, 그 값이 yield의 결과가 된다.

장점과 단점

장점

  1. 메모리 효율적 - 필요할 때만 값을 생성 (lazy evaluation)
  2. 무한 시퀀스 가능 - 무한 루프도 메모리 문제 없음
  3. 실행 제어 - 실행을 멈추고 재개할 수 있음
  4. 이터러블 생성 - for...of로 쉽게 순회

단점

  1. 복잡함 - 일반 함수보다 이해하기 어려움
  2. 디버깅 어려움 - 실행 흐름을 따라가기 힘듦
  3. 성능 - 간단한 작업에는 오히려 느릴 수 있음

실전 사용 예시

ID 생성기

function* idGenerator() {
  let id = 1;
  while (true) {
    yield id++;
  }
}

const getId = idGenerator();
console.log(getId.next().value); // 1
console.log(getId.next().value); // 2
console.log(getId.next().value); // 3

페이지네이션

function* paginate(items, pageSize) {
  for (let i = 0; i < items.length; i += pageSize) {
    yield items.slice(i, i + pageSize);
  }
}

const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const pages = paginate(items, 3);

console.log(pages.next().value); // [1, 2, 3]
console.log(pages.next().value); // [4, 5, 6]
console.log(pages.next().value); // [7, 8, 9]
console.log(pages.next().value); // [10]

제너레이터 vs async/await

과거에는 제너레이터로 비동기를 처리했지만, 지금은 async/await을 쓴다.

// 과거 - 제너레이터
function* fetchUser() {
  const user = yield fetch('/api/user').then(r => r.json());
  const posts = yield fetch(`/api/posts/${user.id}`).then(r => r.json());
  return {user, posts};
}

// 현재 - async/await (훨씬 간단!)
async function fetchUser() {
  const user = await fetch('/api/user').then(r => r.json());
  const posts = await fetch(`/api/posts/${user.id}`).then(r => r.json());
  return {user, posts};
}

결론: 비동기 처리는 async/await, 이터레이션이나 무한 시퀀스는 제너레이터를 사용하자.


관련 문서