본문 바로가기
Programming/자바스크립트

[자바스크립트] 제너레이터

by 코딩하는 랄로 2023. 11. 13.
728x90

javascript - iterable & iterator

 

[자바스크립트] iterable & iterator

iterable 이터러블(iterable)이란 자료를 반복할 수 있는 객체를 말한다. 반복할 수 있는다는 객체가 어떤 의미일까? 반복할 수 있는 객체, 즉 이터러블한 객체는 순회할 수 있는 객체를 의미한다. 우

codingralro.tistory.com

 

제너레이터에 대해서 더 잘 이해하기 위해서는 먼저 이터러블, 이터레이터에 대해서 알고 있어야 한다. 해당 내용에 대해서 모르시는 분들은 위의 블로그를 참고하기 바란다.

 

 

제너레이터(Generator)란

제너레이터란, iterable이면서 동시에 iterator인 함수이다. Promise를 보완하기 위해 async가 나온 것과 같이 제너레이터도 이터레이터의 업그레이드 버전으로 나온 것이다.

 

async가 프로미스를 리턴하는 함수이듯이, 제너레이터 또한 이터레이터를 리턴하는 함수이다. 제너레이터를 사용하면, 기존의 이터러블을 생성하는 방식보다 간편하게 구현할 수 있다.

 

간단한 예시를 보여주면, 기존의 이터러블 프로토콜 방식을 사용한 예제 코드는 아래와 같다.

let range = {
  from: 1,
  to: 5,

  // iterable => Symbol.iterator 함수를 가지고 있는 객체
  [Symbol.iterator]() {
    return {
      current: this.from,
      last: this.to,

      // iterator 함수 => next()가 구현된 객체를 반환
      next() {
        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      }
    };
  }
};

// {from: 1, to: 5, Symbol(Symbol.iterator): ƒ}
// 이 객체는 인자를 from, to 두개 가지고있는 그냥 객체가 아니라,
// 이터레이터가 적용된 특수한 객체이다. 이 객체를 전개연산자 할 경우 순회되어 나타나게된다.
alert([...range]); // 1,2,3,4,5

 

이를 제너레이터를 이용하여 구현하게 되면, 아래와 같은 방법들을 이용하여 간결하게 구현할 수 있다.

// 방식1
const range1 = function* () {
  let i = 1; 
  while(true){ 
    if (i <= 5)
      yield ++i; 
    else
      return;
  }
};

// 방식2
const range2 = {
  from: 1,
  to: 5,

  *[Symbol.iterator]() { 
    for(let value = this.from; value <= this.to; value++) {
      yield value;
    }
  }
};

 

어떻게 사용하는지는 몰라도, 코드가 훨씬 간결해진 것을 볼 수 있다. 

 

 

 

제너레이터 함수 정의

제너레이터 함수는 일반 함수와는 다른 독특한 동작을 하는데, 코드의 블록을 한 번에 실행하지 않고 함수 코드 블록의 실행을 일시 중지했다가 필요한 시점에 재시작할 수 있다.

 

제너레이터 함수의 정의 방식은 일반 함수와 똑같지만 제너레이터의 경우 function 키워드 뒤에 * 를 붙여서 선언하여 준다.

// 제너레이터 함수 선언식
function* genDecFunc() {
  yield 1;
}
let generatorObj = genDecFunc(); // 제너레이터 함수 실행 결과 반환된 제너레이터 객체를 변수에 넣어 사용한다.


// 제너레이터 함수 표현식
const genExpFunc = function* () {
  yield 1;
};
generatorObj = genExpFunc();


// 제너레이터 메소드 식
const obj = {
  * generatorObjMethod() {
    yield 1;
  }
};
generatorObj = obj.generatorObjMethod();


// 제너레이터 클래스 메소드 식
class MyClass {
  * generatorClsMethod() {
    yield 1;
  }
}
const myClass = new MyClass();
generatorObj = myClass.generatorClsMethod();

 

 

 

yield / next

yield는 제너레이터 함수의 실행을 일시적으로 정지시키며, yield 뒤에 오는 표현식은 제너레이터의 caller에게 반환된다. next 메소드는 iterater의 next와 마찬가지로 { value, done } 프로퍼티를 갖는 이터레이터 객체를 반환한다.

 

여기서 value의 값은 yield 문이 반환한 값이고 done은 제너레이터 함수 내의 모든 yield문이 실행되었는지를 나타내는  boolean 타입의 값이다. false일 경우, 아직 실행해야 할 문이 남았음을 뜻한다.

// 제너레이터 함수 생성
function* generateEx() {
  console.log("1st")
  yield 1;

  console.log("2nd")
  yield 2;

  console.log("3rd")
  return 3;
}

// 제너레이터 함수 호출 => 제너레이터 객체를 반환
let iter = generateEx();
console.log(iter.next()); // 1st { value: 1, done: false }
console.log(iter.next()); // 2nd { value: 2, done: false }
console.log(iter.next()); // 3rd { value: 3, done: true }

 

위의 코드를 통해 제너레이터의 다음과 같은 특징을 알 수 있다.

  • 제너레이터 함수를 호출하면 제너레이터 객체를 반환한다. 이때, 제너레이터 객체는 이터러블이며 동시에 이터레이터이기 때문에 Symbol.iterator 메소드로 이터레이터를 별도 생성할 필요가 없다
  • 제너레이터레에서 yield를 통하여 몇 번의 next 를 통해 값을 꺼낼 수 있는지 정할 수 있다.
  • 제너레이터의 실행 결과가 이터레이이터이기 때문에 for..of 역시 사용 가능하다.
  • return을 하면 리턴값을 value와 함께 done이 true가 되면서 순회를 종료 한다.

 

 

yield*

만약 알파벳을 순서대로 소문자 부터 대문자까지 생성하는 제너레이터 함수를 구현한다고 해보자. 간단하게 for문을 이용하여 아래와 같이 구현할 수 있다.

// 제너레이터 함수 생성
function* generateEx() {
  for(let i = 97; i <= 122; i++) { // a : 97, z : 122
    yield i;
  }

  for(let i = 65; i <= 90; i++) { // A : 65, Z : 90
    yield i;
  }
}

let answer = '';
for(let code of generateEx()) {
  answer += String.fromCharCode(code);
}

console.log(answer); // abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ

 

위의 코드에서 for문이 중복되는 것을 볼 수 있다. 이러한 중복을 없애고자 해당 for문은 새로운 제너레이터 함수로 선언하고 이 함수를 yield* 문법을 사용하여 다른 제너레이터 함수에서도 사용할 수 있게 된다.

 

즉, yield* 과 함께 표현된 제너레이터 함수를 호출하여 이터러블 객체를 순회하게 되는 것이다.

function* genFor(start, end) {
  for(let i = start; i <= end; i++){
    yield i;
  }
}

function* generateEx() {
  yield* genFor(97, 122);
  yield* genFor(65, 90);
}

let answer = '';
for(let code of generateEx()) {
  answer += String.fromCharCode(code);
}

console.log(answer); // abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ

 

yield* 뒤에는 이터러블 객체가 오면 순회하기 때문에, 제너레이터 함수(이터러블 객체를 리턴) 이외에도 다른 이터러블한 객체가 와도 된다.

function* generateEx() {
  yield* [1, 2, 3];  // 배열은 iterable 객체
  yield* [1, 2, 3].map(el => el + 3); // map의 결과는 배열!!
}

for(let code of generateEx()) {
  console.log(code);  // 1 2 3 4 5 6
}

// 재귀를 이용하여 반복
(function generateFunc(genFunc) {
  const iter = genFunc();

  (function iterFunc({value, done}) {
    console.log({value, done});
    if(done) return value;
    iterFunc(iter.next(value));
  })(iter.next());
})(generateEx);

/*
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: 4, done: false }
{ value: 5, done: false }
{ value: 6, done: false }
{ value: undefined, done: true }
*/

 

 

 

next(value)

next 메소드에 value 인자를 넘겨주면, 진행을 멈추었던 yield문의 next 메서드에서 받은 인자값으로 치환한 후, 그 위치에서 다시 실행하게 된다.

function* gen() {
  while(true) {
    var value = yield null; // null값을 보내고, next(인자값)을 통해 값을 받는다.
    console.log(value);
  }
}

var g = gen();
g.next(1); // "{ value: null, done: false }"
g.next(2);
// "{ value: null, done: false }"
// 2

 

위의 코드의 흐름은 다음과 같다.

  • next(1) 호출 : 첫번째 호출이므로 제너레이터 함수를 실행하고 첫번째 yield를 만나 null을 반환하고 멈춤 => g.next(1) = { value : null, done : false }
  • 첫번째 호출 시에는 이 전에 멈춘 yield문이 없으므로 넘겨준 인자값이 담길 변수가 존재하지 않음
  • next(2) 호출 : 이전에 멈춘 yield문이 존재하므로 next 메소드의 인자값이 value에 담은 후, 코드 진행
  • console.log를 통해서 value 값 출력 ( 아직 두번째 yield 문 만자지 않은 상태임)
  • 이 후 while(true)를 통해서 다시 반복 => yield문을 만나서 null을 반환 => g.next(2) = { value : null, done : false }

즉, next(value) 를 호출하면, 이전에 진행이 중지되었던 문장을 yield문을 치환한 뒤 해당 문장부터 시작하게 되는데 위의 예제에서 두번째 next문은 다음과 같이 변화된 뒤 진행이 되었던 것이다.

var val = yield null =>   next(value)   =>   var val = value;

 

 

 

전개 연산자

제너레이터 또한 이터러블 객체이자 이터레이터이기 때문에 전개연산자(spread) 를 사용할 수 있다.

function* generateFunc() {
  yield 1;
  yield 2;
  yield 3;
}

// ...Spread
const genSpread = generateFunc();
console.log([...genSpread]); // [ 1, 2, 3 ]

 

 

 

제너레이터 종료

제너레이터는 next의 done이 true가 되면, 순회를 끝내지만, 그 외에도 return과 throw를 통해 종료할 수 있다.

// return
function* genFun() {
  let i = 0;
  try {
    while(true) {
      yield ++i;
    }
  } catch (e) {
    console.log('ERROR : ', e);
  }
}

// return이 호출되면, value에는 return의 인자가 할당
// done => true
let returnGen = genFun();
console.log(returnGen.next()); // { value: 1, done: false }
console.log(returnGen.next()); // { value: 2, done: false }
console.log(returnGen.next()); // { value: 3, done: false }
console.log(returnGen.return(10)); // { value: 10, done: true }


// throw
// throw가 호출되면, catch 블록에  throw의 인자가 전달
let throwGen = genFun();
console.log(throwGen.next()); // { value: 1, done: false }
console.log(throwGen.next()); // { value: 2, done: false }
console.log(throwGen.next()); // { value: 3, done: false }
console.log(throwGen.throw("end")); // ERROR :  end  { value: undefined, done: true }

 

 

 

 

 

 

 

reference : Inpa

728x90