Development Study/Frontend

[JavaScript ES6] function*? yield? 이거 자바스크립트 맞아?? 제너레이터 함수(Generator Functions)를 알아보자

TMInstaller 2023. 12. 12. 14:40
728x90

이 글은 코딩 테스트를 풀다가 다른 사람들의 풀이를 확인하던 도중 신기한 코드를 발견해서 알아본 내용들을 정리한 글이에요. 정말 신기하게 생겨서 공유하고자 이 글을 작성합니다.

제너레이터 함수란 무엇일까요?

이런 상상을 해봐요. 평화로운 오후, 당신은 서재에서 시간을 보내고 있었어요.

평화롭게 읽고싶었던 책을 읽고 있었는데, 갑자기 중요한 전화가 온 거에요.
책에 책갈피를 꽂아두고 잠시 덮어두고 전화를 받았어요. 업무 관런해서 질문이 들어왔었네요.
당신은 대답을 해주고 다시 책을 펼쳐 정확히 읽던 부분부터 읽기 시작했습니다.

제너레이터 함수도 이와 비슷하게 돌아가요. 함수의 실행을 '일시 중지'했다가 '다시 시작'할 수 있다는 것이죠!

어떻게 작동할까요?

function 키워드를 사용합니다. 하지만 조금 다르게 쓰이죠. 예시와 함께 알아볼까요?

function* storyTeller() {
    yield '오랜 옛날, 멀고 먼 왕국에...';
    yield '용감한 기사와 똑똑한 공주가 있었어요.';
    return '그리고 그들은 오래오래 행복하게 살았답니다.';
}

const story = storyTeller();
console.log(story.next().value); // '오랜 옛날, 멀고 먼 왕국에...'
console.log(story.next().value); // '용감한 기사와 똑똑한 공주가 있었어요.'
console.log(story.next().value); // '그리고 그들은 오래오래 행복하게 살았답니다.'


제너레이터 함수는 function* 키워드를 사용해 정의해요.
이 함수 내부에서는 yield 키워드를 사용해 함수의 실행을 중단할 수 있죠.
yield를 만나면, 함수는 그 부분의 결과를 반환하고 실행을 멈춰요.
나중에 다시 실행을 한 번 더 하면? 정확히 멈췄던 지점부터 다시 시작해요.

제너레이터 함수의 용도는 무엇일까요?

여러 방법으로 사용될 수 있어요. 생각보다 신기할거에요.

  1. 비동기 프로그래밍
    • 제너레이터 코드는 비동기 코드를 마치 동기 코드처럼 쉽게 작성할 수 있게 해줘요.
    • 콜백이나 프로미스가 복잡한 작업을 수행하는 코드를 단순화할 수 있어요.
// 제너레이터 함수 정의
function* fetchUserSequence() {
    // 첫 번째 API 호출: 사용자 데이터 불러오기
    const user = yield fetch('/api/user');
    // 두 번째 API 호출: 해당 사용자의 프로필 불러오기
    const profile = yield fetch(`/api/profile/${user.id}`);
    // 세 번째 API 호출: 해당 사용자의 게시물 불러오기
    const posts = yield fetch(`/api/posts/${user.id}`);

    // 모든 데이터 반환
    return { user, profile, posts };
}

// 제너레이터 실행 및 제어 함수
const iterator = fetchUserSequence();
function handleYield(yielded) {
    if (!yielded.done) {
        yielded.value.then(result => {
            // 다음 yield까지 실행
            handleYield(iterator.next(result));
        });
    } else {
        // 모든 작업 완료, 결과 출력
        console.log('모든 데이터를 불러왔습니다:', yielded.value);
    }
}
handleYield(iterator.next());
  1. 데이터 스트림 처리
    • 대용량 데이터를 효율적으로 처리할 수 있어요.
    • 필요할 때만 데이터를 생성하기 때문에 메모리 사용을 최적화할 수 있어요.
function* infiniteNumbers() {
    let n = 1;
    while (true) {
        yield n++;
    }
}

const numbers = infiniteNumbers(); // 무한 시퀀스 생성
console.log(numbers.next().value); // 1
console.log(numbers.next().value); // 2
// 계속해서 다음 숫자를 생성할 수 있지만, 한 번에 하나씩만 메모리에 적재됩니다.
  1. 복잡한 로직 제어
    • 복잡한 반복이나 재귀적인 방법들 조금 더 관리하기 쉽게 구현할 수 있어요.
// 트리 순회를 위한 제너레이터 함수
function* traverseTree(node) {
    // 현재 노드의 값을 반환
    yield node.value;
    // 왼쪽 자식 노드가 있다면 왼쪽 자식 노드를 순회
    if (node.left) {
        yield* traverseTree(node.left);
    }
    // 오른쪽 자식 노드가 있다면 오른쪽 자식 노드를 순회
    if (node.right) {
        yield* traverseTree(node.right);
    }
}

// 트리 구조 예시
const tree = { 
    value: 1, 
    left: { value: 2 },
    right: { value: 3 }
};

// 제너레이터를 사용하여 트리 순회
for (let value of traverseTree(tree)) {
    console.log(value); // 1, 2, 3 순서로 출력
}
  1. 중첩된 n차원 배열을 1차원 배열로 바꾸는 경우
    • 이 문제를 풀다가 발견하게 된 코드로, 신기함을 느끼고 있어요.
const inorderTraversal = function* (arr) {
  // 중첩 배열을 순회하는 도우미 제너레이터 함수
  function* traverse(node) {
    // node가 배열인 경우, 각 서브 배열에 대해 재귀적으로 traverse 호출
    if (Array.isArray(node)) {
      for (let subArray of node) {
        yield* traverse(subArray);
      }
    } else {
      // 배열이 아닌 요소는 그대로 yield(반환)
      yield node;
    }
  }

  // 주어진 배열에 대해 중위 순회 시작
  yield* traverse(arr);
};
/**
 * 사용 예:
 * const gen = inorderTraversal([1, [2, 3]]);
 * gen.next().value; // 1
 * gen.next().value; // 2
 * gen.next().value; // 3
 *
 * 이 코드는 [1, [2, 3]]과 같은 중첩 배열을 입력으로 받아,
 * 1, 2, 3 순서로 각 요소를 하나씩 반환하는 제너레이터를 생성합니다.
 * gen.next()를 호출할 때마다 배열의 다음 요소를 반환합니다.
 */

기억해 두세요!

제너레이터 함수는 책갈피와 같아요!

제너레이터 함수는 책을 읽는 것과 같아서 yield라는 책갈피를 가지고 있죠.
또는 마리오 게임처럼 여러 세이브포인트를 가지고 있는 것과 같다고 볼 수도 있어요.
아무튼 중간에 저장할 수 있는 것이 있는 어떤 것이든 제너레이터 함수처럼 작동한다고 생각해보세요!


End

728x90