Web/[JS] Common

Generator Function - function*

ihl 2021. 11. 11. 21:18
function* generator() {
  yield 1;
  yield 2;
}

const g = generator(); // what is this?

  javascript에서 함수를 선언할 때 *을 붙이는 경우를 종종 볼 수 있다. 이런 함수가 어떻게 동작하는지 알아보고 어떻게 활용할 수 있는지 살펴보자.

 

1. Generator & Iterateration

  MDN에서는 function* 선언은 generator function을 정의하는 데 사용하며, 이 함수는 Generator 객체를 반환한다고 정의되어 있다. 그렇다면 Generator란 무엇일까? Generator 객체란 반복자(Iteration) 프로토콜을 준수하는 객체이다. 그리고 Iteration Protocol은 iterable과 iterator 2가지로 분류된다. 이 단어들의 의미를 하나씩 해결해나 가보자.

 

 1) Iteration

  지금까지 코딩을 하면서 한 번쯤은 'its return value is not iterable', 혹은 'x is not iterable'이라는 에러를 본 경험이 있을 것이다. 이런 오류는 보통 배열이나 문자열이 아닌 다른 타입이나 객체를 for-of 등으로 반복하려 할 때 발생한다. 즉, Iteration이란 것은 배열, 유사 배열과 같은 Collection 데이터와 관련이 있다. 그렇다면 Iterable과 Iterator란 무엇일까? 

 

  Iterator와 Iterable에 대해 어떤 사람은 Iterable이란 도서관에서 책장을 의미하며, Iterator는 도서관의 사서와 같다고 비유한다. 책장은 연속된 책들이 모여있다. 도서관의 사서는 언제든지 특정 책에 접근하여 책을 꺼낼 수 있으며, 도서관 이용자가 다음 권이 있나요?라고 물어봤을 때 사서는 다음 책이 있는지 없는지를 알려준다. 

 

  JavaScript에서 Iterable은 반복 가능한 객체를 의미하며, Iterable 객체가 되기 위해선 속성으로 [Symbol.iterator]가 있고, '@@iterator' 이라는 메서드를 구현해야 한다. 대표적으로 Array, String, Map, Set 등이 Iterable이며, 일반적인 객체는 Iterable 이 아니다.

 

  JavaScript에서 Iterator 객체는 next()라는 메서드를 가지고 있고, next()는 value와 Iterator가 반복 작업을 마쳤는지를 의미하는 done을 가진 객체를 리턴한다. 즉, next()라는 메서드를 통해 Iterable의 값을 탐색하고 Iterable의 값이 끝인지를 리턴하는 것이다.

 

 2) Generatior

function* generatorFn() {  // generator를 반환하는 generator 함수
  yield 1
  yield 2
  yield 3
}

const obj = generatorFn(); // 생성된 generator
console.log(obj.next()); // { value: 1, done: false }
console.log(obj.next()); // { value: 2, done: false }
console.log(obj.next()); // { value: 3, done: false }
console.log(obj.next()); // { value: undefined, done: true }

  Generatior는 Iteration 프로토콜을 준수하는 객체이며, generatior function으로부터 반환된 값이다. generator 함수는 generator를 이용하여 함수의 실행을 중간에 멈추거나 재개할 수 있다. 이러한 generator 객체의 동작은 컬렉션을 순환하는 것과 비슷하다.

 

  generator 함수에는 yield라는 키워드가 존재하며, 생성된 generator의 next() 메서드 호출 시 yield라는 키워드가 나올 때까지 함수를 실행하다가 멈춘다. 그 후 yield에 있는 값과, 함수가 모두 실행되었는지를 리턴한다. 이는 next() 메서드가 호출될 때마다 반복한다. 함수 내에 도달할 수 있는 yield 키워드가 더 이상 없다면 done의 값은 true가 된다.

 

2. Generator 특징

 1) yield

function* generatorFn() { 
  console.log('before 1');
  yield 1
  console.log('after 1');
  console.log('before 2');
  yield 2
  console.log('after 2');
  console.log('before 3');
  yield 3
  console.log('after 3');
  return 'end!';
}

const obj = generatorFn();

console.log(obj.next()); // before 1 -> {value: 1, done: false}
console.log(obj.next()); // after 1 -> before 2 -> {value: 2, done: false}
console.log(obj.next()); // after 2 -> before 3 -> {value: 3, done: false}
console.log(obj.next()); // after 3 -> {value: undefined, done: true}
console.log(obj.next()); // {value: 'end!', done: true} -> {value: undefined, done: true}

  위에서 설명한 Generator의 동작을 더 자세히 살펴볼 수 있는 코드이다. Generator는 next()가 호출되기 전까지 함수의 동작을 미리 수행하지 않는다. 이는 보통의 컬렉션이 값을 미리 만들어 놓는 것과는 차이가 있다. 즉, Generator는 필요한 순간에 값을 계산해서 전달할 수 있는 장점이 있다.

 

 2) instance

function* generatorFn() {  // generator를 반환하는 generator 함수
  yield 1
  yield 2
  yield 3
}

const g1 = generatorFn();
console.log(g1.next()); // {value: 1, done: false}
console.log(g1.next()); // {value: 2, done: false}

const g2 = generatorFn();
console.log(g2.next()); // {value: 1, done: false}

  하나의 generator 함수로부터 generator 인스턴스를 여러 개 생성했을 경우, 각각의 진행상황은 공유되지 않는다. generator 함수는 매번 새로운 generator 인스턴스를 생성한다.

 

 3) return, throw

function* generatorFn() { 
  yield 1
  yield 2
  yield 3
}

const g = generatorFn();
console.log(g.next()); // {value: 1, done: false}
console.log(g.return('a')); // {value: 'a', done: true}
console.log(g.throw('error!')); // Uncaught error!

  generator는 next() 외에도 return()과 throw() 메서드를 사용할 수 있다. 

 

4) throw Error

function* generatorFn() {
  throw new Error('error!');
}

const g = generatorFn();
try {
  g.next();
} catch(e) {
  console.log(e); // Error: error!
}

  generator는 generator 객체를 생성할 때가 아니라 값이 필요할 때 함수 내부의 코드를 실행한다. 따라서 generator 함수의 내부에서 발생한 에러를 잡기 위해선 next() 메서드를 실행하여 값을 가져오는 코드에 try-catch를 사용해야 한다.

 

3. Generator 사용

function * generator(iter, cb) { // 1. 함수선언
  for(const v of iter) {
    yield cb(v);
  }
}

const g1 = generator([1,2,3], (v) => v*2); // 2. 객체생성 : 객체 생성O, 단, 값을 생성하지 않는다.
console.log([...g1]); // 3. 연산 :  ...연산을 통해 값이 생성되었다. ->  [2, 4, 6] 

const g2 = generator('abc', (v) => v+'a');
console.log([...g2]); // ['aa', 'ba', 'ca']

  generator를 이용하면 map, filter, take와 같은 함수형 프로그래밍의 대표적 함수들을 구현할 수 있다. 위 코드는 Array.prototype.map 메서드를 generator를 이용하여 구현한 코드이다. 이 코드를 generator가 아닌 방식으로 구현했다면 함수의 원본을 변경하거나, 새로운 결과 배열 객체를 생성했을 것이다. 또한 generator 함수를 호출했을 때 generator 객체만 생성되고 실제 연산이 수행되지 않는다. 실제 연산은 연산 결과가 필요한 순간에만 수행된다. 이처럼 필요한 순간에만 연산하는 방식을 지연 평가(lazy evaluation)이라고 부른다.

 


MDN: https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/function*

참고 영상(Eng) : https://www.youtube.com/watch?v=IJ6EgdiI_wU 

iterator/iterable : https://stackoverflow.com/questions/6863182/what-is-the-difference-between-iterator-and-iterable-and-how-to-use-them

책 : https://book.naver.com/bookdb/book_detail.nhn?bid=16391022