제너레이터(Generator) 함수

Javascript에서는 동기와 비동기가 있다. 동기는 호출과 실행을 같이 하고 비동기는 호출과 실행을 같이 하지 않는다. 예를 들어 console.log(‘hello’)는 호출하자마자 스스로 ‘hello’를 출력한다. 하지만 hello()는 호출을 하지만 function hello(){ console.log(‘hello’); }에서 ‘hello’를 출력한다.

제너레이터 함수란 이런 비동기 함수를 좀 더 유용하게 사용할 수 있게 하는 함수이다.

간단한 예제와 함께 알아보자

function* call() {
  console.log('첫번째');
  yield 1; // 첫번째 호출                 
  console.log('두번째');
  yield 2; // 두번째 호출                       
  console.log('세번째'); // 세번째 호출 
}

const result = call();

result.next(); // 첫번째
result.next(); // 두번째
result.next(); // 세번째

첫번째
두번째
세번째

제너레이터 함수는 호출 됐을 때 코드를 한번에 실행하지 않고 일시정지 했다가 필요한 시점에서 이어서 나머지 코드를 동작하는 방으로 작동한다.
.next()로 함수를 호출하면 코드에서 yield을 만날 때 실행을 멈추는 방식으로 동작한다. 한 번 호출하면 첫번째 yield까지 실행되고 멈추었다가 두번째 호출되면 첫번째에서 멈춘 곳 부터 두번째 yield을 만날 때까지 실행된다.

.next()로 호출할 때 result처럼 변수에 담지않고 호출한다면 호출된 횟수를 저장할 수 없어 몇 번을 호출해도 ‘첫번째’만 출력된다.

function* call() {
  console.log('첫번째');
  yield 1;                 
  console.log('두번째');
  yield 2;                  
  console.log('세번째'); 
}

call().next(); // 첫번째
call().next(); // 첫번째
call().next(); // 첫번째

첫번째
첫번째
첫번째

또 제너레이터 함수는 .next()로 호출할 때 {value: 1, done: false} 객체를 반환한다.
value는 yield에 할당한 값이고, done은 남은 코드 안에 yield이 있다면 false 없다면 true가 할당된다.

function* call() {
  console.log('첫번째');
  yield 1;                 
  console.log('두번째');
  yield 2;                     
  console.log('세번째');                   
}

const result = call();

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

첫번째
{value: 1, done: false}
두번째
{value: 2, done: false}
세번째
{value: undefined, done: true}

이러한 제너레이터 함수를 쓰는 이유는 무엇일까
가장 큰 이유는 콜백지옥(Callback hell)에 빠지지 않기 위해서이다.

예를 들어 setTimeout을 이용해 3초뒤 30을 출력후 다시 1초뒤 10을 출력, 마지막으로 2초뒤 20을 출력하여 30, 10, 20 순으로 출력하는 코드를 작성해본다고 하자

setTimeout(()=>{console.log('30')}, 3000);
setTimeout(()=>{console.log('10')}, 1000);
setTimeout(()=>{console.log('20')}, 2000);

10
20
30

동기 함수라면 위의 코드로 원하는 값을 얻을 수 있을 것이다. 그러나 setTimeout은 비동기 함수이기 때문에 호출한 순서와 출력된 순서가 맞지않아 원하는 값을 얻을 수 없다.

setTimeout(()=>{
	console.log(30);
	return setTimeout(()=>{
		console.log(10);
    	        return setTimeout(()=>{console.log(20);}, 2000);
        }, 1000);
}, 3000);

30
10
20

원하는 값을 얻기 위해서는 위의 예제처럼 콜백 안에 콜백을 사용해 코드를 작성해야한다. 하지만 이런 콜백함수가 반복되면 가독성이 떨어지고 난해한 코드가 되어 콜백지옥에 빠지게된다.
이 예제에 제너레이터 함수를 적용해보자

function* setime() {
    setTimeout(()=>{console.log('30'); gen.next();}, 3000);
    yield 30;
    setTimeout(()=>{console.log('10'); gen.next();}, 1000);
    yield 10;
    setTimeout(()=>{console.log('20');}, 2000);
}

const gen = setime();

gen.next();

30
10
20

제너레이터 함수를 적용한 결과이다. 눈으로만 보기에도 제너레이터 적용 전의 함수보다 가독성이 높아졌다.

또한 제너레이터 함수에 매개변수를 넣을 수도 있다.

function* gen(n) {
  let x, y;
  x = yield n; 
  y = yield n;

  console.log(x+y); // 3
}
let generator = gen();

generator.next();  // 제너레이터 함수 시작
generator.next(1); 
generator.next(2); 

제너레이터 함수 사용법

  • 제너레이터 함수
function* genFun() {
  yield 1;
}

let generator = genFun();
  • 제너레이터 메소드
let genObj = {
  * genMethod() {
    yield 2;
  }
};

let generator = genObj.genMethod();
  • 제너레이터 클래스 메소드
class MyClass {
  * genClsMethod() {
    yield 3;
  }
}

let genClass = new MyClass();
let generator = genClass.genClsMethod();