누구나 자바스크립트 제너레이터 함수가 필요한 이유는 무엇인가요?

DevBoku·2022년 12월 8일
37

Article 번역

목록 보기
8/8
post-thumbnail

원문 : https://jrsinclair.com/articles/2022/why-would-anyone-need-javascript-generator-functions/

티스토리 블로그 : 티스토리

제너레이터는 자바스크립트 언어의 이상한 부분입니다. 그리고 어떤 사람들은 제네레이터를 약간 수수께끼라고 생각합니다. 당신은 수십 년 동안 성공적인 개발자고, 제너레이터를 알 필요가 없다고 느꼈을 수도 있습니다. 만약 여러분이 제너레이터를 필요로 하지 않고 그렇게 오래 버텨왔다면 의문이 듭니다. 제너레이터는 무엇에 도움이 될까요?

제너레이터에는 재미있는 문법이 있습니다. 제너레이터에는 이상한 별표가 있는 함수로 정의하고, 화살표 함수로는 제너레이터를 정의할 수 없습니다. 제너레이터는 이해하기 힘든 yield 키워드도 가지고 있습니다. 만약 제너레이터가 하는 일에 익숙하지 않다면 코드를 읽기 힘들 수 있습니다.

문제 중 일부는 제너레이터가 저수준 구조라는 것입니다. 즉, 제너레이터는 도구를 만들기 위한 도구와 같으며 일상적인 문제를 해결하기 위해 만든 도구입니다. 하지만 제너레이터 함수를 따로따로 보면 왜 이런 기능이 필요한지 이해하기 어려울 수 있습니다.

그렇지만, 제너레이터는 상당히 강력한 구문입니다. 대부분의 도구들과 마찬가지로 모든 작업에 제네레이터가 필요하지 않을 수 있습니다. 그러나 특정 작업의 경우에는 삶을 훨씬 편하게 만듭니다. 도구를 더 잘 이해하면 그 도구가 어디에 유용한지 알게 됩니다. 마찬가지로 제네레이터를 잘 이해하면 언제 사용하지 않을 때인지 알게 됩니다.

그렇다면 제너레이터는 어디에 좋은 건가요?

팀탐 그리고 지연 이터레이터 (lazy iterators)

제너레이터에는 많은 용도가 있습니다. 그러나 가장 직접적이고 확실하게 적용해보는 건 지연 이터레이터를 만들어 보는 것입니다.

자바스크립트의 이터레이션 프로토콜에 대해 이미 잘 알고 있기를 바랍니다. 이를 통해 자바스크립트 엔진에게 일부 객체가 for...of 루프 또는 spread 문법으로 사용할 수 있단걸 알릴 수 있습니다.

우리가 할 수 있는 가장 간단한 일은 몇 가지 값을 생성하는 제너레이터 함수를 정의하는 것입니다. 그런 다음 for...of 루프를 사용하여 반복해서 처리할 수 있습니다.

function* culturalAchievements() {
    yield 'Amazing coffee';
    yield 'The Sydney Opera House';
    yield 'The invention of Wi-Fi';
}

for (achievement of culturalAchievements()) {
    console.log(`호주는 이것으로 유명합니다: ${achievement}`);
}

// 호주는 이것으로 유명합니다: Amazing coffee
// 호주는 이것으로 유명합니다: The Sydney Opera House
// 호주는 이것으로 유명합니다: The invention of Wi-Fi

그리고 표면적으로는 그것은 무의미해 보입니다. 배열을 사용하면 훨씬 적은 수고로 모든 작업을 수행할 수 있습니다. 하지만 아이러니하게도 제너레이터가 배열과 비슷하다는 것이 제너레이터를 매우 유용하게 만듭니다.

그 이유를 설명하기 위해 호주의 문화적 성취의 정점을 소개해야 합니다. 와이파이의 발명품이 아닙니다. 시드니 오페라 하우스도 아닙니다. 최상급 커피도 아닙니다. 틀림없이 호주의 가장 위대한 문화적 업적은 팀탐(Tim Tam)입니다.

팀탐은 "두 개의 맥아 비스킷으로 구성되어 있으며 가볍고 단단한 초콜릿 크림으로 채워져 있고 질감이 있는 얇은 초콜릿 층으로 코팅되어 있습니다". 이 "초콜릿 맛의 크리미한 벽돌"이 우리의 비유 대상이 될 것 입니다. 팀탐은 처리하려는 데이터 항목을 나타냅니다.

그리고 우리는 팀탐 슬램(Tim Tam Slam)이라는 의식을 통해 팀 탐을 '처리'합니다. 단계는 다음과 같습니다.

  1. 하나의 팀탐을 선택합니다.
  2. 끝에서 2–5 mm 떨어진 한쪽 모서리에서 작은 덩어리를 물어보세요.
  3. 대각선 반대쪽 모서리에서 물기를 반복합니다.
  4. 물린 모서리 중 하나를 뜨거운 음료에 넣습니다. 밀로(Milo)는 전통적이지만 커피, 차 또는 핫 초콜릿도 허용됩니다).
  5. 반대쪽 모서리에 입술을 대고 마치 빨대처럼 팀 탐을 통해 액체를 빨아들입니다.
  6. 액체가 입에 들어가는 즉시 팀탐 전체를 섭취하세요. 팀탐 형태가 무너지기 전에 이를 신속하게 수행하는 것이 중요합니다.
  7. 팀탐 더 이상 없을 때까지 또는 컨디션이 안좋다고 느낄 때까지 반복합니다.

호주 가수 Natalie Imbruglia가 Graham Norton Show에서 시연하고 있습니다.

일부 사람들은 호주와 영국 악센트를 이해하기 어렵다고 생각할 수도 있습니다. 그런 경우라면 Neil de Grasse Tyson이 최근 유튜브 비디오에서 자세히 설명하고 있습니다.

이제 컨디션이 나빠지기 전까지 최대 5개의 팀탐을 섭취할 수 있다고 칩시다. 이 과정을 자바스크립트를 사용하여 나타낸다면 다음과 같습니다.

const MAX_TIMTAMS = 5;

function slamTimTamsArray(timtams) {
    return timtams
        .map(biteArbitraryCorner)
        .map(biteOppositeCorner)
        .map(insertInBeverage)
        .map(drawLiquid)
        .map(insertIntoMouth)
        .slice(0, MAX_TIMTAMS);
}

우리는 팀탐 슬램 과정을 일련의 논리적 단계로 설명했습니다. 그리고 이것은 좋은 일입니다. 그러나 이 함수를 작성하는 방식에는 심각한 문제가 있습니다. 원래 팀탐의 표준 상자에는 11개의 비스킷이 들어 있습니다. 상자를 배열처럼 처리하면 비스킷을 하나씩 꺼내서 모서리를 깨물고 새 상자에 넣습니다. 그런 다음 비스킷을 하나씩 차례로 가져와서 대각선으로 반대쪽 모서리를 깨물었습니다. 그리고 우리는 두 귀퉁이가 잘린 11개의 비스킷을 가지게 됩니다. 하지만 우리가 다음 단계를 시도할 때, 모든 것이 우스꽝스러워집니다. 거기서 우리는 11개의 비스킷을 음료에 넣을 것입니다.

.slice()을 먼저 실행하도록 순서를 바꿔도 여전히 문제가 생깁니다. 한 번에 5장의 비스킷이 들어간 음료가 되어 버립니다. 또한 flow()를 사용해서 .map() 연산의 모든 함수를 함께 구성할 수도 있습니다. 그것과 .slice()를 시작점으로 이동하는 것을 조합하면 상황이 개선됩니다.

const flow = (...fns) => x0 => fns.reduce(
    (x, f) => f(x),
    x0
);

function slamTimTamsUsingFlow(timtams) {
    return timtams
        .slice(0, MAX_TIMTAMS)
        .map(flow(
            biteArbitraryCorner,
            biteOppositeCorner,
            insertInBeverage,
            drawLiquid,
            insertIntoMouth));
}

이 접근 방식도 그리 나쁘지 않지만 첫 번째 시도만큼 깔끔하고 명료하지는 않습니다. 그러나 제너레이터는 지연되기 때문에 첫 번째 예제에서 깔끔한 시퀀스를 유지할 수 있는 방법을 제공합니다. 그러나 작동하려면 몇 가지 유틸리티 함수를 정의해야 합니다. 먼저 Array의 .map() 메서드에 대응하는 것을 정의합니다. 다음과 같이 표시됩니다.

const map = f => function*(iterable) {
  for (let x of iterable) yield f(x);
};

function*() 구문에 유의하십시오. 이 구문은 이 map() 함수가 제너레이터를 반환한다는 것을 의미합니다. 그리고 제너레이터는 이터러블 프로토콜을 구현합니다. 이것은 우리가 반환 값을 가져와서 다른 for...of 구조에서 사용할 수 있음을 의미합니다. 그리고 아이템 수를 제한하는 또 다른 함수를 만들 수 있습니다.

const take = n => function*(iterable) {
  let i = 0;
  for (let x of iterable) {
    if (i >= n) return;
    yield x;
    i++;
  }
};

pipe()의 도움을 조금 받아서 이 두 가지 유틸리티 함수를 사용하면 문제를 해결할 수 있습니다.

const pipe = (x0, ...fns) => fns.reduce(
    (x, f) => f(x),
    x0
);

function slamTimTamsWithGenerators(timtams) {
    return pipe(timtams,
        map(biteArbitraryCorner),
        map(biteOppositeCorner),
        map(insertInBeverage),
        map(drawLiquid),
        map(insertIntoMouth),
        take(MAX_TIMTAMS)
    );
}

이제 배열 메서드 버전과 마찬가지로 팀탐 처리에 대한 제대로 된 설명을 할 수 있었습니다. 제너레이터는 지연되기 때문에 머그잔에 5개의 비스킷으로 끝나지 않을 것입니다. 단, 이 기능에는 문제가 있습니다. 현재 형식에서는 slamTimTamsWithGenerators()를 실행해도 아무 작업도 수행되지 않습니다. 이는 제너레이터가 너무 게을러서 아무것도 하지 않기 때문입니다. 즉, 값을 가져오기 위해 어떠한 행동도 취하지 않는 한 말입니다. 이를 수행하는 일반적으로 두 가지 방법이 있습니다.

  1. spread 구문을 사용하여 제너레이터 객체를 배열로 변환합니다.
  2. 값을 yield 하지 않고 생성기를 반복합니다.

두 번째 방법을 사용하면 배열 메서드 .forEach()와 매우 유사한 forEach() 유틸리티를 작성할 수 있습니다. 이것은 다음과 같습니다.

const forEach = effect => function(iterator) {
  for (let x of iterator) {
    effect(x);
  }
}

이 유틸리티에는 별표 또는 yield 키워드가 없습니다. 단지 effect가 있고 이터레이터의 각 값으로 effect를 실행합니다. 이를 통해 다른 가상 함수 eat()를 파이프라인에 추가하면 새 유틸리티를 사용할 수 있습니다.

function slamTimTamsAndEat(timtams) {
    return pipe(timtams,
        map(biteArbitraryCorner),
        map(biteOppositeCorner),
        map(insertInBeverage),
        map(drawLiquid),
        map(insertIntoMouth),
        take(MAX_TIMTAMS),
        forEach(eat)
    );
}

이제 우리 함수는 5개의 팀탐을 올바른 순서로 먹을 것입니다. 그러나 여전히 처리를 5개(즉, MAX_TIMTAMS)로 제한합니다.

이 '지연' 기능은 제너레이터 함수를 유용하게 만드는 첫 번째 기능입니다. 데이터를 처리하는 방식을 변경할 수 있게 해줍니다. 예를 들어, 한 번에 하나의 항목을 메모리에 로드하여 대용량 데이터 세트를 처리할 수 있습니다. 그리고 이것만으로도 제너레이터를 흥미롭게 만들기에 충분합니다. 그러나 지연에는 다른 이점이 있습니다.

무한 이터레이터 (infinite iterators)

1990년대에 Arnotts는 팀탐의 광고 시리즈를 진행했습니다. 이들 중 첫 번째 시리즈는 케이트 블란쳇이 지니를 만나는 장면이었습니다. 그리고 그녀는 끊임없이 팀탐 상자를 바라는 캐릭터였습니다.

전체 광고 시리즈는 무한한 팀탐 상자라는 개념에 초점을 맞췄습니다. 그리고 끊임없는 팀탐 상자와 마찬가지로 제너레이터는 무한 이터레이터를 만들 수 있습니다.

예를 들어 무한 시퀀스를 제공하는 제너레이터 함수를 만들 수 있습니다.

function* allTheOnes() {
    while (true) { 
        yield 1;
    }
}

console.log([...take(7)(allTheOnes())]);
// [ 1, 1, 1, 1, 1, 1, 1 ]

이제 흥미로울 수도 있지만 아마도 그다지 유용하지 않을 것입니다. 그러나 반복하려는 값을 지정해서 좀 더 일반적으로 만들 수 있습니다.

function* repeat(val) {
    while (true) {
        yield val;
    }
}

console.log([...take(3)(repeat('팀탐'))]);
// [ '팀탐', '팀탐', '팀탐' ]

그래도 그다지 유용하지 않은 것 같습니다. 그러나 이것을 사용하여 다른 시퀀스를 만들 수 있습니다. scanl()이라는 함수를 정의했다고 가정합니다. Array의 .reduce() 메서드와 약간 비슷하게 작동합니다. 그러나 단일 값을 반환하는 대신 일련의 값을 생성합니다.

function* scanl(reducer, initalVal, iterator) {
  let b = initalVal;
  yield b;
  for (const x of iterator) {
    b = reducer(b, x);
    yield b;
  }
}

그리고 scanl()을 사용하여 일련의 자연수를 생성할 수 있습니다.

const add = (a, b) => a + b;
const genNat = pipe(
    repeat(1),
    (ones) => scanl(add, 0, ones)
);

하지만 사실 다음과 같이 작성하는 것이 더 쉽습니다.

function* genNat() {
  for (let i = 0; true; i++) yield i;
}

여기서 요점은 양의 정수 지연 목록을 생성하는 가장 효과적인 방법을 보여주는 것이 아닙니다. repeat()scanl()과 같은 도구를 사용하면 단순한 것에서 복잡한 무한 시퀀스를 만들 수 있습니다.

하지만 양의 정수의 무한 목록을 생성하는 것이 그다지 흥미롭지 않다는 점은 인정하겠습니다. 정말 도움이 되는 것을 해봅시다. 예를 들어, 암호화 또는 임의적인 모습을 만드는데 유용합니다. 우리는 일련의 소수를 생성할 것입니다.

이를 하기 위해서 헬퍼 함수가 두 개 더 필요합니다. 먼저 생성된 시퀀스를 필터링하려고 합니다.

function* filter(p, xs) {
  for (const x of xs) if (p(x)) yield x;
}

그리고 두 번째 유틸리티 함수인 pop()은 다음과 같습니다.

const pop = (iterator) => {
  const result = iterator.next();
  if (result.done) return;
  return [result.value, iterator];
}

pop() 함수는 제너레이터(및 일반적으로 이터레이터)의 약점을 드러냅니다. 약점은 변경 가능하다는 것입니다. 시퀀스에서 한 항목을 pop하면 더 이상 원래 시퀀스를 가져올 수 없습니다. 제너레이터로 작업할 때 주의해야 할 사항입니다. 하지만 지금은 이 두 가지 헬퍼 함수가 있으므로 소수 제너레이터를 함께 사용할 수 있습니다. 그리고 에라토스테네스의 체라고 불리는 기법을 사용합니다. 알고리즘은 다음과 같이 작동합니다.

  1. 2부터 시작하는 모든 자연수 목록으로 시작합니다.
  2. 목록의 첫 번째 요소를 선택한 다음 목록에서 해당 숫자의 배수를 모두 제거합니다.
  3. 2단계로 이동하여 목록의 다음 번호로 반복합니다.

자바스크립트에서 이 작업을 수행하기 위해 두 가지 함수를 만듭니다. 하나는 숫자 목록을 걸러내는 작업을 수행하고 다른 하나는 모든 것을 시작하는 것입니다. 걸러내기 위한 체질(sieving) 함수는 다음과 같습니다.

function* sieve(nums) {
  const result = pop(nums);
  if (!result) return;
  const [n, rest] = result;
  yield n;
  yield* sieve(filter(x => ((x % n) !== 0), rest));
}

yield* 키워드에 주의해 주세요. 이터러블 객체가 있는 경우 yield*는 "이 외의 이터러블에서 모든 것을 yield한다"를 말합니다. 이를 통해 배열의 .flatMap()과 같은 작업을 수행할 수 있습니다.

이제 sieve 함수가 준비되었으므로 2부터 시작하는 모든 자연수 목록으로 시작해야 합니다. 또 다른 유틸리티 함수의 도움을 받아서 이를 생성할 수 있습니다.

function* drop(n, iterable) {
  let i = 0;
  for (const val of iterable) {
    if (i >= n) yield val;
    else i++;
  }
}

그리고 이제 이 모든 것을 하나로 합칠 수 있습니다.

const primes = () => sieve(drop(2, genNat()));
console.log([...take(9)(primes())]);
// [ 2, 3, 5, 7, 11, 13, 17, 19, 23 ]

물론 소수를 생성하는 더 효율적인 다른 방법이 있습니다(아마도). 그러나 여기서의 요점은 소수에 관한 것이 아닙니다. 오히려 무한 시퀀스를 통해 문제에 대해 다르게 생각할 수 있는 방법을 보여줍니다. 또한 다음과 같은 애플리케이션에서 지연 및 무한 시퀀스를 사용할 수 있습니다.

  • 일련의 고유 식별자 생성
  • 게임에서 가능한 모든 움직임을 생성합니다.
  • 많은 순열 및 조합 중에서 특정 값(또는 값)을 찾습니다.

이런 모든 유틸리티 기능이 내장되어 있지 않은 이유는 무엇인가요?

지금까지의 모든 예제를 통해 우리는 우리만의 작은 유틸리티 함수를 작성했습니다. 여기에는 map(), take(), forEach(), filter(), scanl()drop()과 같은 함수가 포함됩니다. 또한 제너레이터에 배열과 같은 임베디드 메서드가 없는 것은 조금 번거롭습니다. 그렇게 느끼는 사람은 당신뿐만이 아닙니다. 이것이 ECMAScript 표준에 이터레이터 헬퍼를 추가하기 위한 2단계 TC39 제안이 있는 이유입니다.

그 사이에 스스로 헬퍼를 만들고 싶지 않다면 도움이 되는 라이브러리를 찾을 수 있습니다. 일반적인 예로 다음 두 가지가 있습니다.

  • Itertools (인기 있는 Python 라이브러리 기반)
  • IxJS (RxJS와 유사하지만 이터러블용).

좀 더 가벼운 것을 찾고 있다면 제 개인 툴킷인 Dynamo를 살펴볼 수 있습니다. 단, 여기서 사용하는 일반적인 자바스크립트가 아니라 타입스크립트로 작성되어 있다는 점을 유의해 주세요.

메시지 전달

제너레이터의 지연은 매력적이고 유용합니다. 그러나 이것이 제너레이터가 할 수 있는 전부는 아닙니다. 제너레이터를 사용하면 한 쌍의 함수 간에 메시지를 양방향으로 전달할 수 있습니다. 솔직히 말하면 함수는 이미 이 작업을 수행할 수 있습니다. 매개 변수를 제공해서 호출된 함수에 메시지를 전달합니다. 그런 다음 호출 함수는 반환 값을 통해 다시 메시지를 받습니다. 그러나 그것은 일회성입니다. 처음에는 한 방향으로 하나의 메시지를 받고 마지막에는 다른 방향으로 메시지를 받습니다. 하지만 제너레이터를 사용하면 많은 메시지를 주고받을 수 있습니다.

가장 일반적인 예는 async/await 구문을 모방하는 것입니다. 예를 들어 Node 코드를 작성한다고 가정합니다. 우리가 작성하는 함수는 다음과 같아야 합니다.

  • 파일에서 설정정보를 읽습니다.
  • fetch 호출을 만들어 인증 토큰을 얻습니다.
  • 일부 데이터를 얻기 위해 토큰으로 또 다른 fetch를 호출합니다.

async/await를 사용하면 다음과 같이 보일 수 있습니다.

const getData = async () => {
  const cfg = await readConfig();
  const authDetails = extractAuthDetails(cfg);
  const token = await fetchAuthToken(authDetails);
  const data = await secureFetch(DATA_URL, token);
  return data;
};

그러나 async/await가 있기 전에는 제너레이터를 사용하여 다음과 같이 할 수 있었습니다.

const getData = asyncDo(function*() {
  const cfg = yield readConfig();
  const authDetails = extractAuthDetails(cfg);
  const token = yield fetchAuthToken(authDetails);
  const data = yield secureFetch(DATA_URL, token);
  return data;
});

const promiseForData = getData();

그리고 그것은 async/await 버전처럼 보입니다. 그렇지 않나요? 하지만 제대로 작동하려면 약간의 추가 작업이 필요합니다. 추가 작업은 다음과 같습니다.

const asyncDo = (runTasks) => (...args) => {
  // runTasks()는 예를 들어 getData()와 같은 제너레이터 함수가 될 것으로 예상하고 있습니다.
  // 먼저 제너레이터 객체를 얻기 위해 실행합니다.
  const generator = runTasks(...args);
  // 다음으로 제너레이터 함수에서 생성된 Promise를 확인하는 재귀 함수를 만듭니다.
  const resolve = (next) => {
    // 모든 재귀 함수에서 작업이 완료되었는지 확인해야 합니다. 
    // 여기에서 제너레이터 결과에서 반환된 `done` 속성을 확인합니다.
    if (next.done) return Promise.resolve(next.value);
    // 우리는 `next.value`가 항상 promise라고 가정합니다. 
    // 아직 완료하지 않은 경우 값을 가져오고 .next()를 사용해서 제너레이터로 다시 전달합니다.
    return next.value.then(data => resolve(generator.next(data)));
  }
  // resolve()를 호출하여 프로세스 전체를 시작합니다.
  return resolve(generator.next());
};

이건 깔끔합니다. 하지만 아마 그렇게 유용하지 않을 것입니다. 우리 대부분은 async/await를 모방할 필요가 없습니다. 하지만 그렇게 말해도 코드를 오래된 브라우저에서 작동시키기 위해 아마 Babel(또는 유사한 것)을 사용하고 있을 것입니다. 이 경우 트랜스파일러는 이런 제너레이터를 사용해서 async/await을 작동하게 합니다. 그리고 제너레이터는 매우 저수준이기 때문에 .next()에서는 할 수 없는 방법으로 이 yield 패턴을 만질 수 있습니다. 이를 통해 작성자는 async/await를 넘어서는 흥미로운 라이브러리를 만들 수 있습니다. 예를 들어, Kyle Simpson의 CAF (Cancellable Async Flows)가 있습니다.

CAF [...] 는 비동기 함수처럼 처리하지만 토큰을 통한 외부 취소를 지원하는 function* 제너레이터용 래퍼입니다. 이렇게 해서 동기로 보이는 취소 가능한 비동기 로직의 흐름을 표현할 수 있습니다. 여전히 취소 가능한 비동기 처리가 (아직) 필요하지 않을 수 있습니다. 그래도 promise를 조작하는 것 외에도 제너레이터의 메시지 전달에는 많은 것이 있습니다. 예를 들어 오류 처리를 단순화하는 데 사용할 수 있습니다.

오류 처리를 추상화하기 위해 전달되는 메시지

이전에 둘 중 하나를 사용해서 오류를 처리하는 방법에 대해 쓴 적이 있습니다. Either 구조를 사용해서 실패할 수 있는 작업의 결과를 나타낼 수 있습니다. 규칙에 따라 Left라는 클래스를 사용해서 실패를 나타냅니다. 그리고 Right라는 클래스를 사용해서 성공을 나타냅니다. 다음은 두 개의 Either 클래스의 단순화된 버전입니다.

class Left {
  constructor(val) { this._val = val; }
  map() { return this; }
  flatMap() { return this; }
  isLeft() { return true; }
}

class Right {
  constructor(val) { this._val = val; }
  map(fn) { return new Right(fn(this._val)); }
  flatMap(fn) { return fn(this._val); }
  isLeft() { return false; }
}

// 편의를 위한 생성자 함수
const right = x => new Right(x);
const left = x => new Left(x);

이제 해당 기사에서 CSV 파일을 구문 분석하는 예를 살펴보았습니다. 그리고 단일 행을 구문 분석하기 위해 다음과 같은 함수를 생각했습니다.

function processRowChained(headers, row) {
  return right(row)
    .map(splitFields)
    .flatMap(zipRow(headers))
    .flatMap(addDateStr);
}

이것은 Either를 반환하는 함수입니다. 그러나 제너레이터와 메시지 전달을 사용하면 yield를 사용해서 Either를 약간 덮어쓸 수 있습니다. 다음과 같이 보입니다.

const processRow = eitherDo(function* (headers, row) {
  const fieldData = splitFields(row);
  const rowObj = yield zipRow(headers)(fieldData);
  const withDateStr = yield addDateStr(rowObj);
  return withDateStr;
});

이 경우 제너레이터 함수가 항상 Either를 생성할 것으로 예상합니다. 그리고 eitherDo 함수는 그렇게 작동합니다.

// eitherDo()는 함수를 반환하는 함수입니다.
const eitherDo = (genFn) => (...args) => {
  // 전달된 인수를 사용해서 제너레이터 함수를 호출하여 메시지 전달을 시작합니다.
  const generator = genFn(...args);
  // 다음으로 제너레이터에서 처음 생성된 값을 가져옵니다.
  let next = generator.next();

  // `done` 응답을 받을 때까지 계속 값을 처리합니다.
  do {
    // 반환 값이 Either가 아닌 일반 값이라고 가정하므로 Right로 래핑합니다.
    if (next.done) return right(next.value);

    // Left를 만나면 나머지는 모두 건너뛰고 Left 구조를 반환합니다.
    if (next.value.isLeft()) return next.value;

    // 아직 여기에 있는 경우 제네레이터에서 Right를 얻었으므로
    // 여기서 값을 추출하여 제너레이터 함수로 전달합니다.
    next = generator.next(next.value._val);
  } while (!next.done);
}

결국 우리는 processRow()에서 Either 값을 다시 얻습니다. processRow()processRowChained()와 같은 일을 합니다. 그러나 이런 코드 스타일은 특히 연결된(chained) 메서드 호출에 익숙하지 않은 일부 사용자에게 더 편안하게 느껴질 수 있습니다.

마무리

제너레이터에 익숙하지 않다면 처음에는 조금 이상하게 보일 수 있습니다. 그리고 제너레이터는 매우 저수준 언어이라서 다양한 애플리케이션에 사용할 수 있습니다. 그렇기 때문에 당신이 매일 제너레이터가 필요한지 알기 어려울 수 있습니다. 그러나 지금까지 살펴본 것처럼 제너레이터는 다음과 같은 작업에 도움이 됩니다.

  • 대용량 데이터 세트를 효율적으로 처리
  • 무한 시퀀스 작업
  • 두 함수 사이에 메시지를 전달

이제 제너레이터를 알 필요가 없다고 느꼈을 수도 있습니다. 여러분이 하고 있는 일이 제너레이터에 적합하지 않을 수도 있습니다. 그러나 만일을 대비해서 도구 상자(toolbox)에 넣어두면 편리합니다. 그리고 살펴보기 시작하면 많은 곳에서 은밀히 작동하는 제너레이터를 찾을 수 있습니다. 그리고 가끔 라이브러리 코드를 파헤쳐야 할 수도 있습니다. 이러한 경우 제너레이터가 어떻게 작동하는지 이해해 두면 좋습니다.

profile
Front-End Engineer

2개의 댓글

comment-user-thumbnail
2022년 12월 13일

제너레이터는 저수준 언어는 아니지 않나요?
비선점형 스케쥴링을 단순히 코드레벨에서 추상화한 개념이라고 생각되는데요.

답글 달기
comment-user-thumbnail
2023년 6월 9일

좋은 번역 감사합니다.
중간에 소수메이커 만드는 부분은

function* drop(n, iterable) {
//
let i = 1; // 1로 변경
for (const val of iterable) {
if (i >= n) yield val;
else i++;
}
}

function* genNat() {
// natural numbers
for (let i = 1; true; i++) yield i; // 1부터 생성
}

이렇게 변경해 줄 필요가 있다고 생각합니다. 그 이유는 함수 이름을 genNat이라고 했는데 Natural number generate 같은 이름을 지어두고 정수인 0부터 생성하고 있고, 이를 변경함에 따라 drop도 변경해야 합니다.

답글 달기