JS Generator function

불꽃남자·2020년 9월 29일
1

오늘은 JS의 Generator에 대해 알아보는 시간을 가져보자.

Generator

Generator 객체는 Generator function을 호출하면 반환되는 객체이다. Generator 객체는 이터러블 객체이며 동시에 이터레이터이기 때문에, 커스텀 이터러블을 만들기가 좀 더 용이해진다.

Generator function

Generator function은 function* 구문으로 선언할 수 있다.

function* generator(i) { //제네레이터 함수 선언
  console.log("yield 1");
  yield i++;
  console.log("yield 2");
  yield i++;
  console.log("yield 3");
  yield i;
  console.log("yield 4");
}

const gen = generator(1); //제네레이터 함수를 호출하여 제네레이터 생성

console.log(Symbol.iterator in gen); //이터러블 객체이기 때문에 true

gen.next(); //동시에 이터레이터이기 때문에 next메소드 사용 가능
gen.next();
gen.next();
gen.next();


제네레이터 함수가 반환한 이터레이터에 next()메소드를 사용하면, 제네레이터 함수가 yield문을 만날 때 까지 실행되고, 함수가 일시중지된다. 그리고 해당 yield문이 명시하는 값을 이터레이터의 반환값으로써 반환한다.
next()메소드를 실행했는데 함수가 끝날 때 까지 yield문이 없다면 {value: undefined, done: true}를 반환한다.

yield문은 제네레이터 함수 안에서 함수의 동작을 중지시키고, yield문 뒤에 오는 값을 caller(제네레이터 함수를 호출한 함수)에게 반환한다.
MDN에선 yield문에 대해 제네레이터 버전의 return문이라고 표현하고 있다.
이것은 무슨 이야기인가 하면 yield문은 단순한 연산자이지, 어떠한 값이 아니라는 이야기이다.
아래의 코드를 보자.

function* generator(i){
  const a = yield i;
  console.log(`a: ${a}`);
}

const gen = generator(1);

gen.next();


첫 번째로 next()메소드를 실행했을 때, yield i;가 실행되고 {value: 1, done: false}가 반환된다.
두 번째로 next()메소드를 실행하면 const a에는 yield i;가 담기게 되고, console에 a를 출력하고 함수는 종료된다. 그런데 a는 여전히 undefined인 상태이다.
왜냐하면 yield i는 caller에게 i를 반환했지, a에게 반환하고 있는 게 아니기 때문이다.

어찌보면 당연한 이야기이지만, 확실히 짚고 넘어가지 않으면 헷갈리기가 쉽다.
yield문은 뒤에 오는 값을 caller에게 반환한다.

그리고 next()메소드는 하나의 인자를 받을 수 있다. 인자로 전달된 값은 일시중지되었던 위치의 yield문의 자리를 차지하게 된다. 무슨 이야기인지 아래 코드를 살펴보자.

function* generator(){
  const a = yield 1;
  console.log(`a: ${a}`);
  const b = yield 2;
  console.log(`b: ${b}`);
  const c = yield 3;
  console.log(`c: ${c}`);
}

const gen = generator();

gen.next("첫 번째 next인자");
gen.next("두 번째 next인자");
gen.next("세 번째 next인자");
gen.next("네 번째 next인자");


처음 이 코드를 작성할 때에, 나는 첫 번째 next의 인자값이 첫 번째 yield에 대입될 것이라 생각했다. 근데 a에는 두 번째 next의 인자값이 들어있다.
곰곰히 생각해봤는데 이것은 const a = yield 1;이 실행될 때에 왼쪽에서 오른쪽으로 실행된다고 착각하고 있어서 그렇다. 오른쪽의 yield 1이 실행되고, 그 결괏값이 cosnt a 에 들어가는 것이 실행 순서이다.

첫 번째 next()메소드가 실행되면 yield 1을 만나게 되고, 제네레이터가 일시정지되고 {value: 1, done: false}를 caller에게 반환한다.
두 번째 next()메소드가 실행되면 제네레이터 함수가 yield 1에서부터 재개된다. 그리고 재개된 yield에 next()메소드의 인자값이 대입된다. 그래서 const a = "두 번째 next인자" 가 되는 것이다. 그 뒤 console.log(`a: ${a}`);를 출력하고 yield 2를 만나 {value: 2, done: false}를 caller에게 반환한다. yield 3 또한 마찬가지이다.

그래서 왜 제네레이터를 쓰는가?

제네레이터가 뭐 하는 녀석인지는 알았다. 그렇다면 왜 제네레이터를 사용하는가?

그 이유는 바로 느긋한 평가(lazy evaluation) 때문이다.

느긋한 평가

느긋한 평가라. 이름만으로는 오히려 성능이 더 떨어질 것 같은 뉘앙스이다. 하지만 프로그래밍에서 느긋한(lazy)이라는 단어는 대개 어플리케이션의 성능에 대해 긍정적인 효과를 가지고 있다.

보통 함수를 호출하면 그 즉시 함수가 실행되고, return문을 만나면 return문 뒤의 값을 반환하고 종료된다.

function justFunc(len){
  const arr = [];
  for(let i=0; i<len; i++){
    arr.push(i);
  }
  return arr;
}

console.log(justFunc(3));

function* generator(len){
  const arr = [];
  for(let i=0; i<len; i++){
    arr.push(i);
    yield arr;
  }
}

const gen = generator(3);
console.log(gen);

gen.next();
gen.next();
gen.next();

justFunc()를 호출하면 즉시 함수가 실행되고 종료된다. 하지만 제네레이터 함수를 호출하면 제네레이터 객체를 반환하고, 제네레이터 객체는 제네레이터 함수와 yield문을 조합해서 일시정지/재개 컨트롤을 할 수 있다.
코드를 보면 justFunc()는 호출되면 인자로 받은 숫자만큼의 길이의 배열을 즉시 생성해서 반환하고, generator()는 호출되면 제네레이터 객체를 반환하고, 제네레이터 객체의 next()를 호출할 때마다 길이가 1씩 늘어나는 배열을 반환한다.

그래서 이게 어디에 쓰이며 어느 부분에서 메모리 효율이 좋아지는 걸까?
나의 생각으로는 인스타그램이나 페이스북같은 서비스에 쓰일 수 있다고 생각한다. 인스타그램이나 페이스북은 스크롤을 페이지의 끝까지 내리면 계속해서 게시글들이 로드되어 화면에 보여지는데, 제네레이터 안에 while(true)같은 무한 반복문 내에 데이터 요청-렌더링 코드를 넣어두고, 페이지가 끝나면 next()를 사용해 데이터를 요청하고 렌더링 해주는 것이다.
당장 생각나는 것은 이것밖에 없다. 하지만 분명 프론트엔드 공부를 하다 보면 제네레이터가 필요할 때가 올 것이다.

profile
프론트엔드 꿈나무, 탐구자.

0개의 댓글