JavaScript - 제너레이터

박정호·2022년 10월 5일
0

JS

목록 보기
18/24
post-thumbnail

⭐️ Generator

제너레이터는 well-formed(잘 형성된) 이터레이터를 반환해주는 함수이자 편리하게 이터레이터를 생성하는 함수이다.

  • 제너레이터 또한 iterable 및 iterator 프로토콜의 조건을 만족하고 잘 정의된 iterable이다.

  • 제너레이터 함수를 사용하면 이터레이션 프로토콜을 준수해 이터러블을 생성하는 방식보다 간편하게 이터러블을 구현할 수 있다.

1️⃣ 제너레이터 사용이유

✏️ 비동기 특성을 동기적 코드방식으로 관리해준다.

자바스크립트는 성능을 위해 비동기를 채택한다. 콜백함수는 비동기를 관리하는 가장 쉽고 흔한 방법입니다.

하지만 콜백을 중첩하게 되면 콜백지옥이라는 코드 가독성의 문제가 발생합니다. 이때 프로미스가 등장해 함수 중첩을 함수의 연쇄적 호출표현으로 변경했다.

그러나 then( ),catch( )함수가 이중,삼중으로 겹치게 되면 이 또한 다른 가독성의 문제를 일으킨다.

그래서 Async/await가 등장한다. 이것은 비동기적 구조를 동기적으로 작성할 수 있게도와준다.

제네레이터 함수도 동기적으로 작성할 수 있는데, 사실 Async/await 는 제네레이터 기반해 만들어졌다.

💡 async/ await & generator

✏️ 이터레이터와 이터러블을 쉽게 사용 가능

핵심은 직접 Symbol.iterator(), next()메서드 하나씩 작성해서 사용하는 것과 미리 구현된 기능을 사용하는 것의 차이이다.

제네레이터 함수는 이터레이터의 특성이 이미 정리되어 포함되어 있다.
제네레이터가 반환하는 객체는 제네레이터 객체지만 같은 이터레이터 객체가 포함되어 있고,
그래서 next( )메서드를 호출 할 수 있는 이유이다.

  • 직접 next() 메서드와 Symbol.iterator( )을 입력한 경우
const iterableObj = {
  [Symbol.iterator]() {  
    let step = 0;
    return {
      next() {
        step++;
        if (step === 1) {
          return { value: 'This', done: false};
        } else if (step === 2) {
          return { value: 'is', done: false};
        } else if (step === 3) {
          return { value: 'iterable.', done: false};
        }
        return { value: '', done: true };
      }
    }
  },
}
for (const val of iterableObj) {
  console.log(val);
}
  • 제네리이터를 사용한 경우
function * iterableObj() {
  yield 'This';
  yield 'is';
  yield 'iterable.'
}
for (const val of iterableObj()) {
  console.log(val);
}

✏️ 제너레이터 함수는 코루틴(corountine) 특성을 갖는다.

좀 어려운 개념인 거 같아 참고 하자 ㅠㅠ

✏️ 제너레이터 함수는 동시적인 특성을 갖는다

“실행-정지-실행-정지”의 코루틴 형태를 잘 이용하면, 협력형 멀티태스킹 방식으로, 쓰레드 프로그래밍 없이 동시성 프로그래밍이 가능해진다.

쓰레드는 필요한 비용에 비해 신경써야 할 것들이 너무 많은 단점이 있지만, 코루틴은 OS의 암묵적인 스케쥴링이나 컨텍스트 스위칭 오버헤드, 세마포어 설정 같은 고민으로 부터 자유롭다.

✏️ 제너레이터 함수는 비동기적 특성을 갖는다.

동기적으로 작동되는 코루틴 특성이 비동기 코드를 작성하는데 도움을 줄 수 있다.이는 코드가 작성 된 형태는 동기적이나 실행되는 방식은 비동기라는 의미한다.

비동기 프로그래밍의 대표적인 방식인 콜백함수는 단계를 거듭할 수록 코드의 가독성이 떨어지는 콜백지옥을 경험하게 된다. 예를 들어 Async/await 경우 비동기적 특성을 동기적인 형태로 작성한다.

✏️ 메모리 효율에 기여할 수 있다.
제네레이터 함수는 느긋한 계산(Lazy-Evaluation)을 통해 필요 할 때 값을 요구하기 사용해서 메모리를 효율적으로 사용할 수 있다.

느긋한 계산이란 값을 받기 까지 그 시간을 지연시키고 늦추는 것을 말한다. 즉 값이 필요하지 않을 때는 가만히 있다가 필요할 때가 되면 값을 요구하는 것이다.

yield를 값을 넘겨주거나 저장하고, 값이 필요할 때 next( )메서드 호출해 느긋하게 시간을 지연하며 작업 할수 있다.

1️⃣ 제너레이터 함수 사용

✏️ 제너레이터 함수는 function * 키워드를 사용

  • 제너레이터 함수는 화살표 함수를 사용할 수 없다.
  • 별표 *(asterisk)의 위치는 function키워드와 함수 이름 사이면 아무데나 붙여도 되지만, 일관성 유지 목적으로 function키워드 바로 뒤에 붙이는 것을 권장한다.
function* generatorFunc1(){ ... }

const generatorFunc2 = function* () { ... }

✏️ 제너레이터 함수는 함수 호출자에게 함수 실행의 제어권을 양도(yield) 가능

  • 일반 함수의 경우 함수 호출자는 함수 호출을 하면 함수에 대한 제어는 호출된 함수 본인에게 있다.

  • 하지만 제너레이터 함수의 경우, 함수 호출자에게 함수에 대한 제어를 양도 할수 있어서, 함수 호출자는 힘수 실행을 일시 중지시키거나 다시 시작하게 하도록 할 수 있다.

✏️ 제너레이터 함수는 함수 호출자와 함수의 상태를 주고 받는다

  • 일반 함수의 경우 외부에서 매개변수를 통해 값을 전달 받고 실행된다. 즉, 함수 실행 동안에는 외부의 값에 의해 함수의 상태를 변경할 수 없다.

  • 하지만, 제너레이터의 경우, 함수 호출와 양방향으로 함수의 상태를 주고 받을 수 있다. 즉, 함수 호출자에게 자신의 상태를 전달하거나 함수 호출자로부터 추가적인 상태를 전달받을 수 있다.

✏️ 제너레이터 함수는 호출 시 제너레이터 객체를 생성해 반환

  • 일반 함수의 경우 호출이 되면 코드 블록이 실행된다.

  • 하지만, 제너레이터 함수의 경우 제너레이터 객체를 생성해서 반환한다.

2️⃣ 제너레이터 객체

🧐 그렇다면 제너레이터 객체란 무엇인가?

제너레이터 객체란 제너레이터 함수를 호출했을 때 반환되는 객체로, 제너레이터 객체는 이터러블이자 이터레이터이다.

제너레이터 객체는 이터레이터이기 때문에 next 메서드 사용이 가능하다. 그리고 이터레이터는 없는 메서드를 추가로 사용 가능하다.

3️⃣ yield & next & return & throw

✏️ yield

yield는 제너레이터를 멈추거나 다시 실행하는데 사용된다. yield는 표현식(expression)으로도 작성할 수도 있는데 표현식을 작성할 경우 왼쪽에 결과값을 할당한다.

const job = yield "What do you do?";

표현식을 작성하지 않을 경우 undefined를 반환하며 할당하는 순간은 next()를 호출하여 전달한 파라미터 값이 할당된다. 이러한 방식을 통해 함수가 실행 되고 있는 도중에 외부와 소통이 가능하다.

제너레이터 함수에서는 yield 키워드가 있을때 까지 실행되므로 yield 이전에 있는 평가문들의 실행을 멈출수는 없다.

💡 yield*
: yield를 일괄 처리

function* generateAll() {
  yield* ["Call", "Me"];
}

const it = generateAll();

console.log(it.next()); // { value: 'Call', done: false }
console.log(it.next()); // { value: 'Me', done: false }

✏️ next

이터레이터에서 활용된 next 를 제너레이터에서 동일한 개념으로 사용할 수 있다. 한가지 달라진 점은 next를 통해 값을 제너레이터로 전달할 수 있다는 점이다.

  1. it 변수에 call 제너레이터 함수를 실행하면 이터레이터를 반환 하고 멈춤 상태가 된다.

  2. next를 호출하고 파라미터로 undefined를 넘겨주면 제너레이터는 "What is your phone number?"을 넘기고 멈춤 상태가 된다.

  3. next를 호출하고 파라미터로 "123-12345"를 넘겨주면 제너레이터는 해당 값을 할당 후 return한다.

function* call() {
  const phoneNumber = yield "What is your phone number?";

  return `I'm calling to ${phoneNumber}`;
}

const it = call();

console.log(it.next());
console.log(it.next("123-12345"));

// 결과
{ value: 'What is your phone number?', done: false }
{ value: "I'm calling to 123-12345", done: true }

😃 여기서 알 수 있는 것은 next 다음에 전달한 값을 변수에 할당하고 그 다음 순서로 실행한다는 점이다. 그 이유는 처음 next를 호출 할 경우 첫 행을 실행하지만 yield가 있으므로 다시 제어권을 넘겨주기 때문이다. 첫 행을 완료하기 위해선 다시 next를 호출 해주어야 하며 이때 전달된 값을 할당한다.

✏️ return

generator.return(parameter)은 인자로 전달받은 parameter를 value의 값으로, true를 done으로 하는 iterator result object를 리턴한다.

yield 문은 return을 사용하지 않으면 제너레이터를 끝내지 않는다. 그 말은 return을 사용할 경우 yield 문이 남아있더라도 제너레이터를 종료하고 값을 반환한다는 뜻이다.

function* finish() {
  const a = yield;
  const b = yield a;
  const c = yield b;
  return c;
}

const it = finish();

console.log(it.next());
console.log(it.next(10));
console.log(it.return(30));
console.log(it.next(20));

//결과
{ value: undefined, done: false }
{ value: 10, done: false }
{ value: 30, done: true }
{ value: undefined, done: true }

만약 return 메서드를 활용하지 않고 next를 사용할 경우

console.log(it.next()); // { value: undefined, done: false }
console.log(it.next(10)); // { value: 10, done: false }
console.log(it.next(30)); // { value: 30, done: false }
console.log(it.next(20)); // { value: 20, done: false }

✏️ throw
generator.throw(parameter)는 인자로 전달받은 에러를 발생시키면서, value값으로는 undefined, done값으로 true를 갖는 iterator result object를 리턴한다.

throw는 return과 마찬가지로 실행 즉시 종료 시키며 다른 점은 Error를 발생시킨다는 점이다.

function* warning() {
  try {
    yield 10;
    yield 20;
    yield 30;
  } catch (error) {
    console.error(error);
  }
}

const it = warning();

console.log(it.next()); // { value: 10, done: false }
console.log(it.throw("Whoops.."));
// Whoops..
console.log(it.next()); // { value: undefined, done: true }
console.log(it.next()); // { value: undefined, done: true }

제너레이터를 사용할때 try...catch문으로 에러를 잘 핸들링 하는 것이 중요하다.

💡 중요) 이터러블(iterable) & 이터레이터(iterator)

이터러블
: 이터러블은 이터러블 프로토콜을 준수한 객체를 뜻한다. 즉, Symbol.iterator를 프로퍼티로 사용한 메서드를 직접 구현하거나, 프로토타입 체인을 통해 상속받은 것을 말한다.

  • 배열
  • 문자열
  • Map
  • Set

이터레이터
: 이터러블에 Symbol.iterator 메서드를 호출했을 때 반환되는 값이다. 이터레이터는 next라는 메서드를 가지고 있고, 이를 통해 이터러블을 각 요소로 순회할 수 있다.

const arr = [1,2,3]
const iterator = arr[Symbol.iterator]()
const iteratorResultObject = iterator.next()
console.log(iteratorResultObject) // {value: 1, done: false}

**자세한내용:** [이터레이션 프로토콜](https://velog.io/@pjh1011409/JavaScript-%EC%9D%B4%ED%84%B0%EB%9F%AC%EB%B8%94%EC%9D%B4%ED%84%B0%EB%A0%88%EC%9D%B4%ED%84%B0-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C)

4️⃣ 제너레이터 with 코드

✏️ for문을 이용하여 평상시처럼 구현하기

function *evens(l){
  for (let i=0;i<l;i++){
    if (i%2===0) yield i;
  }
}

let iter=evens(10);
c.log(iter.next()["value"]) // 0
c.log(iter.next()["value"]) // 2
c.log(iter.next()["value"]) // 4
c.log(iter.next()["value"]) // 6
c.log(iter.next()["value"]); // 8

✏️ infinity generator을 이용하여, for of식으로 나타내기

  • 보통의 코드에서 while(true)는 무한 루프에 빠지는 위험이 있으므로 사용을 피한다. 하지만 generator안에서는 yeild로 각 단계의 반복을 제어할 수 있기 때문에, while(true)을 활용하여 무한으로 사용 가능한 로직을 만들 수 있다.

//inFinite
function *infinity(i=0){
  while(true) yield i++
}

function *evens(l){
    for(const a of infinity(0)){
      if(a%2===0) yield a;
      if(a===l) return;
    }
}
let iter=evens(10);
c.log(iter.next()['value']);
c.log(iter.next()['value']);
c.log(iter.next()['value']);
c.log(iter.next()['value']);
c.log(iter.next()['value']);

✏️ Limit gerator를 이용하여, Limit도 함수형으로 표현해보고, 전개 연산자와 구조분해할당 이용해보기

function *infinity(i=0){
  while(true) yield i++;
}
function *limit(l,iter){
  for(const a of iter){
    yield a;
    if (a===l) return;
  }
}
function *evens(l){
  for(const a of limit(l,infinity(0))){
    if (a%2===0) yield a;
  }
}

let iter=evens(10);
c.log(iter.next()["value"]);

c.log("for_of문을 이용하여 만들기")
for(const a of evens(20)){
   c.log(a)
}
c.log("구조분해, 전개연산자 이용")

console.log(...evens(10));
c.log("전개연산자: "+[...evens(10)])
c.log("전개연산자2: "+[...evens(10),...evens(20)])

const [head,...tail] = evens(10);
c.log(head)
c.log([tail])

결과

0

for_of문을 이용하여 만들기

0
2
4
6
8
10
12
14
16
18
20

구조분해, 전개연산자 이용

전개연산자: 0,2,4,6,8,10
전개연산자2: 0,2,4,6,8,10,0,2,4,6,8,10,12,14,16,18,20

0
2,4,6,8,10

참조 및 참고하기 좋은 사이트

profile
기록하여 기억하고, 계획하여 실천하자. will be a FE developer (HOME버튼을 클릭하여 Notion으로 놀러오세요!)

0개의 댓글