타입스크립트 제네레이터 이해하기

김동욱·2023년 7월 15일
0
post-thumbnail

원문: https://dev.to/logrocket/understanding-typescript-generators-20lo

일반 함수는 위에서 아래로 진행 후 종료가 됩니다. 제네레이터 함수 또한 위에서 아래로 진행됩니다. 하지만 제네레이터 함수는 실행 도중 멈출 수 있고 같은 위치에서 재개될 수 있습니다. 이 진행은 프로세스가 끝날때까지 진행된 후 끝납니다. 이 문서에서는 타입스크립트 제네레이터 함수를 어떻게 다루는지 몇개의 예시와 사용 예제를 다루겠습니다.

제네레이터 함수를 타입스크립트에서 만들기

일반 함수는 열심히 일하는 반번 제네레이터는 게이릅니다. 이 뜻은 나중에 실행하도록 요청 할 수 있습니다. 제네레이터 함수를 만들면서 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();

비록 일반함수와 작성 및 수행이 같을지라도 제레레이터 함수가 호출 될때 어떠한 로그를 볼 수 없습니다. 간단히 말해 제네레이터는 코드를 실행하지 않습니다. 제네레이터 함수는 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

제네레이터 인터페이스는 Iteratror를 extends합니다, 이건 next메서드를 사용 가능하도록 만듭니다. 또한 [Symbol.iterator] 속성을 가지도록 합니다.

이터러블과 이터레이터 이해하기

제네레이터를 보기전에 이터러블과 이터레이터를 살펴봅시다.

  • 이터러블(iterables): 반복할 수 있는
  • 이터레이터(iterators): 반복자

이터러블 객체는 for...of로 반복할 수 있는 객체를 의미합니다. 이터러블 객체는 Symbol.iterator 메서드를 반드시 가지고 있습니다. 예를 들어 array객체는 자바스크립트의 빌트인 이터러블입니다. 즉 array는 이터레이터를 가지고 있습니다.

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;
   }
}

이터레이터는 이터러블을 반복 가능하도록 만듭니다. 다음 예제는 클로저를 이용하여 이터레이터에 대한 간단한 구현입니다.

function naturalNumbers() {
  let n = 0;
  return {
    next: function() {
      n += 1;
      return {value:n, done:false};
    }
  };
}

const iterable = naturalNumbers();
iterable.next().value; // 1
iterable.next().value; // 2
iterable.next().value; // 3
iterable.next().value; // 4

위에서 언급했듯 이터레이터는 Symbol.iterator속성을 가지고 있는 겍체이다. 즉 만약 우리가 next함수를 반환하는 함수를 만들면, 위 예시처럼 우리의 객체는 자바스크립트 이터러블이 된다. 그리고 for...of를 통해 어디서든 사용이 가능하다.

물론 제네레이터 함수와 위의 예시 사이에는 유사성이 있지만, 실제로 제네레이터가 한번에 한가지 값을 계산하기때문에 우리는 손쉽게 이터레이터를 제네레이터를 사용하여 구현할 수 있습니다.

타입스크립트에서 제네레이터 사용하기

제네레이터에서 놀라운 점은 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) {
    yield n += 2;
  }
}
const gen = evenNumbers();
console.log(gen.next().value); //2
console.log(gen.next().value); //4
console.log(gen.next().value); //6
console.log(gen.next().value); //8
console.log(gen.next().value); //10

제네레이터 사용처

제네레이터는 데이터의 흐름제어를 위한 강력한 방법을 제공하며 유현하며 효과적이고 가독성이 좋은 타입스크립트 코드를 만듭니다. 주문형 값을 생산, 비동기 동작 수행 그리고 커스텀 이터레이션 로직을 만드는 제네레이터의 능력은 몇몇의 시나리오에서 큰 역할을 합니다.

요구된 값 계산하기

우리는 성능 향상을 위해 계산과 요구사항의 값을 산출 그리고 중간값을 캐싱하기 위해 제네레이터를 사용할 수 있. 이 기술은 복잡한 계산을 하거나 그들이 실제로 필요할 때 값의 실행을 종종 늦출때 매우 유용하다.

function* calculateFibonacci(): Generator<number> {
  let prev = 0;
  let curr = 1;

  yield prev;
  yield curr; 

  while (true) {
    const next = prev + curr;
    yield next;
    prev = curr;
    curr = next;
  }
}

// Using the generator to calculate Fibonacci numbers lazily
const fibonacciGenerator = calculateFibonacci();

// Calculate the first 10 Fibonacci numbers
for (let i = 0; i < 10; i++) {
  console.log(fibonacciGenerator.next().value);
  // 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
}

위의 예시에서, 모든 피보나치 수열을 계산하는 대신 오칙 요청할 때만 필요한 피보나치 수열을 계산하고 산출한다. 이 결과로 효과적인 메모리 절약이 되며 필요한 요구사항의 값이 산출되었다.

큰 데이터 반복하기

제네레이터는 한번에 많은 양의 메모리를 데이터에 넣지않고 데이터를 반복하도록 한다. 대신 값을 원할때 생성 할 수 있으며 그렇기때문에 메모리 효과를 올릴 수 있다. 이건 큰 데이터를 처리할때 부분적으로 유용한 방법이다.

function* iterateLargeData(): Generator<number> {
  const data = Array.from({ length: 1000000 }, (_, index) => index + 1);

  for (const item of data) {
    yield item;
  }
}

// Using the generator to iterate over the large data set
const dataGenerator = iterateLargeData();

for (const item of dataGenerator) {
  console.log(item);
  // Perform operations on each item without loading all data into memory
}

위 예시에서 iterateLargeData제네레이터 함수는 만단위 숫자 배열로 만들어진 큰 데이터를 시뮬레이션 해보았다. 한번에 전체 배열을 반환하는 대신 제네레이터는 각 항목을 yield키워드로 각각 한번에 하나씩 산출을 합니다. 그러므로 우리는 메모리에 모든 숫자를 넣을 필요없이 데이터 전체를 순회할 수 있습니다.

제네레이터로 재귀하기

제네레이터 메모리의 효율적인 특성은 디렉토리 내부를 재귀하여 파일 이름을 읽는것처럼 더 유용한 용도로 사용될 수 있다. 실제로 중첩구조를 제귀적으로 도는것은 제네레이터를 생각했을때 자연스럽게 받아들여진다.

yield가 실행될때, yield* 는 다른 이터러블 객체에서 대리인으로 사용된다.

profile
안녕하세요. 부산에서 근무하고 있는 프론트엔드 개발자 김동욱입니다. 영어 공부 겸 개발 공부를 위해서 글을 작성하고있습니다.

0개의 댓글