본문 바로가기

Backend/Node.js

제네레이터 (Generator)

지금의 Node.JS 프로그래밍에선 비동기를 처리할 때 제네레이터(Generator) 를 사용하는 일은 거의 없을 겁니다.

오히려 이게 뭔지도 모르는 개발자가 있을 수도 있습니다. ES2017 부터 나온 async - await 사용이 너무도 간편해졌기 때문이죠.

다만 제네레이터는 async - await 구문이 동작하게 하는 배경이므로 이 동작부터 이해를 해야 할 것 같습니다. 아직도 ts build 를 할 때
target 설정을 ES2015 로 하면 async - await 구문이 래핑된 제네레이터로 떨어지는걸 볼 수 있듯이 말이죠.

 

- 제네레이터 (Generator)

 

제네레이터는 * 연산자와 yield 키워드를 사용해 일시적으로 실행 흐름을 중지시켰다가, 다시 시작시킬 수 있습니다.

 

function* movieGenerator() {
  console.info(1);
  yield 'Black Panther';
  console.info(2);
  yield 'Avengers1';
  console.info(3);
  yield 'Dr.Strange';
  console.info(4);
  return 'all done';
}

const generator = movieGenerator();
console.info(generator.next());
console.info(generator.next());
console.info(generator.next());
console.info(generator.next());

/*
1
{ value: 'Black Panther', done: false }
2
{ value: 'Avengers1', done: false }
3
{ value: 'Dr.Strange', done: false }
4
{ value: 'all done', done: true }
*/

 

기본적인 제네레이터 사용은, function 키워드 다음에 * 연산자를 추가합니다. (function 키워드 다음에 써야한다는 얘기는 화살표 함수를 사용할 경우엔 제네레이터를 만들 수 없습니다) 그리고 함수 내에서 yield 키워드를 사용해 실행을 일시 중지 하고 전달된 값을 반환할 수 있습니다.

 

제네레이터 객체를 생성했을 때 중요한 메서드는 next 입니다. 제네레이터를 시작하거나 재시작할 때 사용되며, 위의 예시에서 볼 수 있듯이

{ value: xxx, done: boolean } 형태의 객체를 반환합니다. value 는 yield 시에 반환되는 값이며 done 은 제네레이터가 끝났는지 여부를 알려줍니다.

 

- with Iterator

 

생성된 제네레이터의 next 메서드를 호출해서 끝까지 실행할 수 있는 특성때문에, 제네레이터는 반복자(Iterator) 를 구현하는데 유용합니다. 

 

function* listGenerator(list) {
  for (let i = 0 ; i < list.length ; i++) {
    yield list[i];
  }
}

const iterator = listGenerator([10, 20, 30, 40]);
let item = iterator.next();

while(!item.done) {
  console.info(item.value);
  item = iterator.next();
}

/*
10
20
30
40
*/

 

iterator 의 next 를 호출할 때마다 제네레이터의 for loop 를 다시 시작합니다. 루프가 다시 시작할 때 모든 변수들의 상태는 실행이 일시 중지 되었을 때와 동일하죠. 실제로 JS 에서 볼 수 있는 Iterator 들은 이렇게 구현되어 있는 케이스가 많습니다.

 

- 비동기 처리

 

위에서 지금은 제네레이터를 많이 쓰지 않고 async - await 구문을 사용한다고 얘기했는데 async - await 구문은 비동기 처리를 할 때 사용하기에, 우리는 제네레이터로도 비동기 처리를 할 수 있습니다.

 

const fs = require('fs');

function wrapper(genFunction) {
  const generator = genFunction((err, data) => {
    if (err) {
      generator.throw(err);
    }
    return generator.next(data);
  });
  generator.next();
}

wrapper(function* (callback) {
  try {
    const result = yield fs.readFile('test.xt', 'utf-8', callback);
    console.info(result);
  } catch (e) {
    console.error(e);
  }
});

 

wrapper 에 제네레이터를 넘겨 실행하는 코드인데, 임의로 생성한 wrapper 함수는 제네레이터를 파라미터로 받아 인스턴스화 해서 바로 실행합니다. 이 wrapper 가 받은 제네레이터는 callback 이라는 인자를 받는데 이 callback 에서의 처리엔 위에서 얘기하지 않은 제네레이터의 기본 동작들이 들어가있습니다.

에러가 있을 때 callback 은 제네레이터의 throw 를 호출하고, 에러가 없을 경우엔 next 메서드로 값을 전달합니다.

제네레이터는 throw 메서드를 통해 에러를 던질 수 있고, 마찬가지로 try - catch 를 통해 이를 핸들링할 수 있습니다.

next 메서드에 값을 같이 전달하면 이는 yield 에 전달됩니다. 따라서 result 에는 next(data) 로 넘긴 data 가 설정됩니다.

 

- co

 

제네레이터를 근래엔 거의 쓸 일이 없어서 관련 라이브러리까지 봐야하나 싶기도 하지만.. 간단히만 살펴보도록 하겠습니다.

위에서 제네레이터로 비동기 제어를 할 수 있다고 썼지만 그냥 저렇게만해선 프라미스에 대해서 제대로 동작하지 않습니다.

위의 예시를 정확히는 callback 처리였다고 봐야겠네요.

 

const readFilePromise = () => {
  return new Promise((resolve, reject) => {
    fs.readFile('test.txt', 'utf-8', (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}

function* genratorFunction() {
  const data = yield readFilePromise();
  console.info(data); // undefined
}

const generator = genratorFunction();
console.info(generator.next()); // { value: Promise{ <pending> }, done: false }
console.info(generator.next()); // { value: undefined, done: true }

 

좀 억지스러운 예시이긴 합니다만.. 제네레이터로는 Promise 의 fulfilled 혹은 rejected 된 값을 반환 받을 수 없습니다.

뭔가 다른 작업을 해줄 수 있겠지만, 대체로 그러는것보다 라이브러리를 사용하는게 훨씬 효율적입니다. 이러한 경우에 co 라는 라이브러리를 사용할 수 있습니다.

 

const readFilePromise = () => {
  return new Promise((resolve, reject) => {
    fs.readFile('test.txt', 'utf-8', (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}

co(function* generatorFunction() {
  const data = yield readFilePromise();
  console.info(data);
}).catch(err => {
  console.error(err);
});

 

사용법은 간단합니다. 기존 제네레이터 함수를 co 로 감싸주면 끝입니다. 이게 가능한 이유는 co 가 yield 로 지정할 수 있는 객체로 Promises 를 지원하기 때문입니다. 그 외엔 Thunks, Arrays, Objects, Generators 등이 있습니다.

이렇게 예시를 보긴 했지만 co 는 v4 버전이 2015 년에 나오고 별다른 업데이트가 없어 그냥 이런게 있구나.. 정도만 확인하면 될 것 같습니다.

 

예전엔 react 에서 redux 를 사용할 때 제네레이터를 사용하기도 했고, 웹 프레임워크 중엔 koa 도 co 로 구성된걸로 알고있었는데 async - await 구문이 Node.JS 에서 그냥 사용할 수 있다보니 지금은 거의 스킵되는 내용 중 하나인 듯 하네요. (koa 도 이젠 코어가 async - await 로 구성되어있다고 얘기하는 듯 합니다)

'Backend > Node.js' 카테고리의 다른 글

모듈과 의존성  (0) 2022.11.12
스트림 (Stream)  (0) 2022.09.03
Promise & Promisify  (0) 2022.08.20
Observer Pattern in Node.js  (0) 2022.08.13
모듈 시스템  (0) 2022.08.07