JS

이터레이터(Iterator), 제너레이터(Generator)

Hyeon_E 2024. 6. 13. 14:04

이터레이터(Iterator)

Enumerable

객체에 자동으로 생성되는 객체속성(property attribute)에는 [[enumerable]]라는 속성이 있음

  • [[enumerable]]은 프로퍼티 열거 가능 여부를 나타냄
  • [[enumerable]]이 true인 객체는 프로퍼티 요소를 열거할 수 있음

Iteration

직역하면 반복으로 객체의 프로퍼티 조회를 반복하기 위해서는 [[enumerable]]이 true여야 함

 

for ... in과 for...of에 대한 문법을 살펴보면 대상이 enumerable 해야한다고 명시되어 있음

 

- for...in은 일반 객체(iterable이 아님)도 순회하지만 for...of는 iterable 요소들만 반복 가능

 

▶ 이터레이션 프로토콜(Iteration Protocols)

다양한 데이터 공급자(array, string, map/set ...)로 부터 순차적인 데이터를 가져오기 위한 효율적인 규칙이 필요해서 탄생

만약 예시의 데이터를 불러올때마다 각자 다른 규칙과 문법이 쓰이면 매우 비효율적

 

반복에 관한 프로토콜

  • iterable protocol: 순회 가능 한
  • iterator protocol: 순회 기능이 있는

 

Iterable Protocol

  • 직관적으로 이해하기 위해 "순회 가능 한" 으로 표현
  • 명시적 규칙은
    • [Symbol.iterator] (=@@iterator) 라는 메서드가 있어서, [Symbol.iterator]를 호출하면 iterator 객체를 반환한다.

명식적 규칙을 만족하는 객체를 iterable 한 객체 라고 함

 

- Symobl은 객체의 속성을 만들 수 있는 데이터 타입이며 Symbol.iterator는 iterble 속성을 정의하기 위해 쓰임

 

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

// for of
for (const item of arr) {
  console.log(item);
}

// for of 동작 과정
const iter = arr[Symbol.iterator]();
let cur;
while (!(cur = iter.next()).done) {
  const item = cur.value;
  console.log(item);
}

 

이터레이터 next 함수를 실행시켜서 value 값을 추출할 수 있음

 

console.log(...iter);
// 1 2 3

 

전개연산자(...)를 사용해서 done이 false일 때 value 값들을 전개할 수 있음

Iterator Protocol

  • 직관적으로 이해하기 위해 "순회 기능이 있는" 으로 표현
  • 명시적 규칙은 
    • next() 메서드를 가지고 있으며, next() 호출 시, {value, done} 형태의 Iterator Result를 반환한다.
    • value에는 현재 순회하는 위치의 값을, done은 해당 객체의 프로퍼티를 모두 순회했는지의 여부를 나타냄

명시적 규칙을 만족하는 객체를 iterator 객체 라고 함

 

Well Formed Iterable

위 두 조건을 동시에 만족하는 객체를 Well Formed Iterable 객체라고 함

  • iterable 한 객체의 [Symbol.iterator]()를 실행하여, iterator를 생성하여 사용(객체가 이터레이터화 되어 반환됨)
  • iterator는 next() 메서드를 통해 사용할 수 있음

예시

const arr = [1, 2, 3]

const iter = arr[Symbol.iterator]() // Array Iterator 객체 생성

console.log(iter.next())
console.log(iter.next())
console.log(iter.next())

console.log(...iter); // 1 2 3

 

  1. [Symbol.iterator]()로 이터러블한 배열을 가지고 이터레이터 객체(현재 값 value와 완료여부done을 가진)를 생성
  2. 이터레이터 객체의 next() 메서드를 활용하여 순회
  3. 만약 전개연산자 (...) 를 사용하게 되면, 이터러블 객체의 이터레이터를 순회하면서, next() 로 각 요소를 하나씩 받아와 나열

 

제너레이터(Generator)

코드블록의 실행을 일시 정지했다가 필요한 시점에 재개할 수 있는 함수
함수 호출자에게 실행의 제어권을 양도 가능하며, 함수의 상태를 주고 받을 수 있음
제너레이터 함수를 호출하면 제너레이터 객체를 반환

제너레이터는 이터레이터를 발생시키는 함수로 사용

 

▶ 제너레이터 형태

일시 정지를 위해서는 yield를 사용

즉, 제너레이트 함수 내부에 yield 키워드를 사용해서 이터러블의 요소를 표현식으로 나타낼 수 있음

yield를 사용하기 위해서는 함수를 generator function로 바꿔야함
generator functionfunction * 으로 표기

 

// 제너레이터 함수선언문
function* genDecFunc(){
  yield 0;
}

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

// 제너레이터 메서드
const obj = {
	* genObjMethod(){
      yield 0;
  }
};

// 제너레이터 클래스 메서드
class genClass{
	* genClassMethod(){
      yield 0;
    }
}

 

function* gen() {
  yield 1;
  yield 2;
}

const g = gen();
console.log(g.next()); // { value: 1, done: false }
console.log(g.next()); // { value: 2, done: false }
console.log(g.next()); // { value: undefined, done: true }

 

gen 함수가 호출되면 제너레이터 객체가 반환되며, next() 메소드를 통해 제너레이터를 실행

제너레이터가 실행되면 일반적인 함수처럼 실행되다가 yield 키워드를 만나면 실행이 중단되고 어디에서 중단되었는지 기억하고 next()를 호출한 곳에 값을 반환해줌

이후에 또다시 next()를 호출하면 기억해두었던 중단시점부터 다시 동작을 시작하여 yield를 만나면 다시 중단하고, next()를 호출한 곳에 값을 반환해 줌(이 동작을 제너레이터가 끝날때 까지 반복)

 

반환 값{ value: any, done: boolean } 형태의 객체로 value는 yield 키워드 뒤에 오는 값이고, done은 제너레이터가 끝까지 실행되었는지를 나타냄

제너레이터를 실행하다가 더 이상 yield를 만나지 못하면 done은 true가 되고, value는 undefined이 됨

 

function* gen() {
  yield 1;
  return 2
}

const g = gen();
console.log(g.next()); // { value: 1, done: false }
console.log(g.next()); // { value: 2, done: true }
console.log(g.next()); // { value: undefined, done: true }

 

return 키워드를 사용하여 값을 반환하면 done은 true가 되고, value는 return 키워드 뒤에 오는 값이

return 키워드를 사용하여 값을 반환하면 제너레이터는 더 이상 실행되지 않으며, next()를 호출해도 done은 true가 되고 value는 undefined가 됨

 

function* gen() {
  yield 1;
  yield 2;
  yield 3;
  return -1;
}

const iter = gen();
for (item of iter) console.log(item);
// 1
// 2
// 3

 

yield 키워드가 이터러블의 요소가 되는 것을 볼 수 있음

마치 배열처럼 yeild 옆에 표현식이 배열의 요소가 되는 거라고 생각하면 쉬움(실제 배열은 아님)

 

▶ 제너레이트 객체 

제너레이터 함수를 호출하면 제너레이터 객체가 반환
제너레이터 객체는 iterator이면서, iterable(Well Formed Iterable)
즉, 내부에 포함된 next() 메서드로 yield를 넘길 수 있음

 

첫 yield 전까지의 코드실행은 next() 메서드가 호출되어야 비로소 실행

처음에는 이터러블 상태인 제너레이터 객체 그 자체만 반환 

next()로 yield 이후를 실행, 실행한 값을 value로 받음, next()는 {value, done} 형태의 iteratorResult를 반환

 

제너레이터 함수는 제너레이터 객체를 만들어 주는 역할일 뿐이고,
함수 호출자가 next()로 제너레이터 객체의 값을 하나씩 넘기면서 실질적으로 활용

 

▶ 활용

제너레이터를 사용하는 이유는 이터레이터를 더 쉽게 구현하기 위해서

이터레이터를 사용하는 이유는 자료를 순회하며 활용하기 위해서

순회 할 때, 다음 자료로 넘어가는 단계를 제어하여 프로그램의 효율을 높일 수 있음

순회보다 연산를 먼저 하도록 순서를 변경하는 것을 지연평가(Lazy Evaluation)

 

- 지연평가(Lazy Evaluation): 계산의 결과값이 필요할 때까지 계산을 늦추는 기법

로직에서 뒤늦게 값이 필요할 때 만들어내는 방식

즉, 지연평가란 식별된 메모리 공간, 메모리 상에 계산된 값(또는 그것의 주소)을 할당하는 과정을 최대한 미루는 행위

 

function* infinity() {
  let i = 0;
  while (true) yield ++i;
}
const iter = infinity();
console.log(iter.next().value); // 1
console.log(iter.next().value); // 2
console.log(iter.next().value); // 3

 

원할때 iter만 공유한다면 언제든지 1이 증가된 수를 받아서 사용할 수 있게 됨

스프레드 연산자for of 문그대로 실행시키면 모두 평가되어 무한 루프가 발생하므로 주의해야함

 

function newArr(n) {
  let i = 1;
  const res = [];
  while (i < n) res.push(i++);
  return res;
}
function* newArrGen(n) {
  let i = 1;
  while (i < n) yield i++;
}
function fiveArr(iter) {
  const res = [];
  for (const item of iter) {
    if (item % 5 == 0) res.push(item);
    else if (res.length == 2) break;
  }
  return res;
}

console.log(fiveArr(newArr(100)));  // [ 5, 10 ]
console.log(fiveArr(newArrGen(100)));  // [ 5, 10 ]

 

같은 결과가 나오지만 제너레이터를 활용한 코드는 좀 더 빠르게 동작(즉시 평가와 지연평가의 차이)

fiveArr(newArr(100))의 경우 newArr 함수가 배열을 즉시 만들어내 만들어진 배열을 리턴(fiveArr([1,2,3,...,99]))

fiveArr(newArrGen(100))의 경우 newArrGen 함수가 이터레이터만 만들어내고 fiveArr 함수에 필요할 때 이터레이터에서 평가된 값을 사용하게 됨

 

확인해보면 즉시 평가와 달리 지연 평가가 빠르게 동작하는 것을 볼 수 있음

console.time('');
console.log(fiveArr(newArr(10000000))); // [ 5, 10 ]
console.timeEnd(''); // : 285.535ms

console.time('');
console.log(fiveArr(newArrGen(10000000))); // [ 5, 10 ]
console.timeEnd(''); // : 7.296ms

 

 

newArrGen 함수에 log를 찍어서 확인해보면 필요할 때만 값을 평가해서 동작하는 모습을 볼 수 있음

 

1
2
3
4
5
6
7
8
9
10
11
[ 5, 10 ]

 

값이 필요할 때 이터레이터에서 꺼내 쓰므로 무한대로 이터레이터를 만들어도 결과는 같음

 

console.time('');
console.log(fiveArr(newArrGen(Infinity))); // [ 5, 10 ]
console.timeEnd(''); // : 7.441ms

 

Reference 

[ JS ] 이터레이터, 제너레이터, 지연평가 ( Iterator, Generator, Lazy Evaluation )

[Javascript] Generator 활용과 장점 (iterable, iterator, lazy evaluation)