aotoyae

[JS] Generator 제너레이터, yield, next(), Symbol.iterator 본문

JavaScript

[JS] Generator 제너레이터, yield, next(), Symbol.iterator

aotoyae 2023. 12. 15. 03:03

 

 

: 함수의 실행을 중간에 멈췄다가 재개할 수 있는 기능

: 다른 작업을 하다가 다시 돌아와서 next() 해주면 멈췄던 부분부터 이어서 실행

+ Redux saga 에서 많이 쓰고 있다..

 

function 옆에 *을 붙여서 만들고

내부에 yield 키워드를 사용한다. 이 yield에서 함수의 실행을 멈출 수 있다!

 

제너레이터 함수를 실행하면 제너레이터 객체가 반환된다.

❗️ 제너레이터 객체엔 next() 메소드가 있는데, 사용해 보자!

function* fn() {
  console.log(1);
  yield 1;
  console.log(2);
  yield 2;
  console.log(3);
  console.log(4);
  yield 3;
  return "finish";
}

const a = fn();
console.log(a); // fn {<suspended>}

console.log(a.next()); // 1 가장 가까운 yield를 만나기 전까지 실행
// {value: 1, done: false} 그리고선 데이터 객체 반환

 

여기서 value는 yield 옆의 값이다!

만약 값을 넣지 않으면 undefined가 반환된다.

 

done은 함수 코드가 끝났는지 나타낸다!

실행이 끝났다면 true, 아니라면 false

function* fn() {
  console.log(1);
  yield 1;
  console.log(2);
  yield 2;
  console.log(3);
  console.log(4);
  yield 3;
  return "finish";
}

const a = fn();
console.log(a); // fn {<suspended>}

console.log(a.next());
// 1
// {value: 1, done: false}
console.log(a.next());
// 2
// {value: 2, done: false}
console.log(a.next());
// 3
// 4
// {value: 3, done: false}
console.log(a.next()); // 이제 함수 끝남
// {value: 'finish', done: true}
console.log(a.next());
// {value: undefined, done: true}

 

❗️ 또 다른 메소드로는 return(), throw()가 있다.

return () (위 함수에서 콘솔 부분만 변경)

console.log(a.next());
// 1
// {value: 1, done: false}
console.log(a.next());
// 2
// {value: 2, done: false}
console.log(a.return("END")); // return을 쓰는 즉시 done 값이 true가 된다.
// {value: 'END', done: true}
console.log(a.next()); // 이후에 next()를 살행해도 value를 얻을 수 없다.
// {value: undefined, done: true}

 

throw() return과 같이 작성하는 즉시 done 값이 true가 된다.

function* fn() {
  try {
    console.log(1);
    yield 1;
    console.log(2);
    yield 2;
    console.log(3);
    console.log(4);
    yield 3;
    return "finish";
  } catch (e) {
    console.log(e);
  }
}

const a = fn();
console.log(a); // fn {<suspended>}

console.log(a.next());
// 1
// {value: 1, done: false}
console.log(a.next());
// 2
// {value: 2, done: false}
console.log(a.throw(new Error("err")));
// Error: errat variable.js:25:21
// {value: undefined, done: true}
console.log(a.next());
// {value: undefined, done: true}

 

제너레이터는 iterable이다! 

❗️ iterable : 반복이 가능하다는 의미

- Symbol.iterator 메소드가 구현되어 있어야 한다.

- Symbol.iterator를 호출한 결과는 iterator라 한다.

 

❗️ iterator 

- {value: , done: } 이 객체를 반환하는 메소드인 next가 있어야 한다.

- 그러니 제너레이터는 itrator이면서 itrable이다.

 

 

그런데, 배열도 반복이 가능하다!

const arr = [1, 2, 3, 4, 5];
console.log(arr);

 

배열도 Symbol.iterator 메소드를 가지고 있다.

이를 활용해 보자.

const arr = [1, 2, 3, 4, 5];
console.log(arr);

const it = arr[Symbol.iterator]();
// it에 arr가 갖고 있는 Symbol.iterator 메소드를 실행한 값을 넣어주었다.

console.log(it.next());
// {value: 1, done: false}
console.log(it.next());
// {value: 2, done: false}
console.log(it.next());
// {value: 3, done: false}
console.log(it.next());
// {value: 4, done: false}
console.log(it.next());
// {value: 5, done: false}
console.log(it.next());
// {value: undefied, done: true}

 

배열은 Symbol.iterator 메소드를 가지고 있고,

이 메소드가 반환하는 값이 iterator이므로

배열은 iterable하다고 할 수 있다!

❗️ 즉 배열은 반복 가능한 객체!

const arr = [1, 2, 3, 4, 5];

for (num of arr) {
  console.log(num);
}
// 1
// 2
// 3
// 4
// 5

 

iterable 위와 같이 for of 문을 이용해 순회할 수 있다.

 

function* fn() {
  yield 4;
  yield 5;
  yield 6;
  return "finish";
}

const a = fn();
console.log(a[Symbol.iterator]() === a); // true

 

제너레이터(a)의 Symbol.iterator 메소드의 실행 값은 자기 자신과 같다.

즉 제너레이터는 iterable 객체

 

그러니 for of 를 쓸 수 있다.

반환된 iterator에 next 메소드를 호출하면서 done이 true가 될 때까지 반복한다.

function* fn() {
  yield 4;
  yield 5;
  yield 6;
  return "finish";
}

const a = fn();
for (num of a) {
  console.log(num);
}
// 4
// 5
// 6

 

그럼 문자열도 될까?

❗️ 된다! str[Symbol.iterator] 콘솔을 찍어보면 메소드를 갖고 있다.

const str = "hi";
const strIterator = str[Symbol.iterator]();
console.log(strIterator.next());
// {value: 'h', done: false}
console.log(strIterator.next());
// {value: 'i', done: false}
console.log(strIterator.next());
// {value: undefined, done: true}

 

for of 를 실행해보면?

❗️ 실행된다! 문자열도 iterable이다!

const str = "hi";
const strIterator = str[Symbol.iterator]();

for (a of strIterator) {
  console.log(a);
}
// h
// i

 

next 메소드에 인수를 전달해 보자.

function* fn() {
  const num1 = yield "첫 번째 숫자를 입력해주세요.";
  console.log(num1);

  const num2 = yield "두 번째 숫자를 입력해주세요.";
  console.log(num2);

  return num1 + num2;
}

const a = fn();
console.log(a.next()); //{value: '첫 번째 숫자를 입력해주세요.', done: false}
console.log(a.next(2));
// 2
// {value: '두 번째 숫자를 입력해주세요.', done: false}
console.log(a.next(3));
// 3
// {value: 5, done: true}

 

이렇게 제너레이터는 외부로부터 값을 입력받을 수도 있다.

 

 

제너레이터는 값을 미리 만들어놓지 않는다.

메모리 관리 측에서 효율적! 필요한 순간에만 연산해서 값을 준다.

아래처럼 while(true)를 사용해 무한 반복자를 만들어도 브라우저가 뻗지 않는다.

function* fn() {
  let index = 0;
  while (true) {
    yield index++;
  }
}

const a = fn();
console.log(a.next());
// {value: 0, done: false}
console.log(a.next());
// {value: 1, done: false}

 

❗️ next를 호출할 때만 값을 주기 때문!

 

일반적인 함수로 어떤 값을 구할 때, 모든 값을 미리 계산해둬야 한다.

또, 쓸지 안 쓸지 정해지지 않은 상황에서도 그 값을 유지해야 한다.

❗️ 그런데! 제너레이터를 사용하면 필요한 순간까지 계산을 미룰 수 있다.

 

 

yield*을 이용해 다른 제너레이터를 불러보자.

function* gen1() {
  yield "w";
  yield "o";
  yield "r";
  yield "l";
  yield "d";
}

function* gen2() {
  yield "Hello,";
  yield* gen1(); // yield* 옆엔 반복 가능한 모든 객체가 올 수 있다.
  yield "!";
}

console.log(...gen2()); // Hello, w o r l d !
// 여기서 쓰인 구조분해할당은 for of 와 마찬가지로
// done 이 true 가 될 때까지 값을 펼쳐주는 역할을 한다.

 

 

 

🔗 https://youtu.be/qi24UqyJLgs?si=4b8BqYiDNKyc3GoA