[완벽가이드] 이터레이터와 제너레이터

쏘쏘임·2022년 6월 12일
0

Basic Javascript

목록 보기
1/2
post-thumbnail

ECMA6에 '유일한 값'인 Symbol이 새로운 원시값으로 추가되었다. 이전엔 가지각색으로 순회하던 방법들을 이 Symbol값을 이용한 이터러블 방식으로 통일하게 되었다.

뿐만 아니라 커스텀 이터러블을 만들어 조작할 수도 있고, 제너레이터를 이용하면 이를 더 단순화시킬 수 있어 API 제작시 매우 유용한 개념이다.

당장 제너레이터를 이용해 뭔가 개발할 상황은 아니지만 이를 활용해 만든 다양한 키워드들과 js 내에서의 데이터 순회 방법을 이해하기 위해 꼼꼼히 읽어보았다.

Intro

ES6 기능으로 추가된 이터러블 객체와 이터레이터는 for/of 루프로 데이터 구조를 순회할 수 있다.

  • for/of 루프로 데이터 구조 순회
for(let i of iterable){
  console.log(i)
}
  • ... 연산자로 분해
	let chars = [..."abcd"]; // chars == ["a", "b", "c", "d"]
  • 분해 할당
	let purpleHaze = Uint8Array.of(255 , 0, 255 , 128); 
	let [r, g, b, a] purpleHaze; // a == 128
  • Map 객체 순회
    - 맵의 반환 값은 [key, value] 쌍
	let m = new Map ( [ ["one" , 1] , ["two" , 2]]); 
	for(let [k,v] of m) console.log(k, v); // ’one 1’, ’two 2’
	
	[...m] // => [["one" , 1] , ["two'’, 2]]: 기본 순회
	[...m.entries()] // => [["one" , 1] , ["two" , 2]]: entries () 메서드도 같다
	[...m.keys()] // => ["one" , "two"]: keys() 메서드는 키만 순회
	[...m.values() ]  // => [1, 2]: values() 메서드는 값만 순회

12.1 이터레이터의 동작 방법

iterable object : 배열, set, map 같은 순회 가능한 타입의 객체 (이터레이터 객체를 반환하는 이터레이터 메서드를 가짐)
iterator object : 순회를 수행하는 객체 그 자체 (next() 메서드를 가짐)
iteration result object : 각 순회 단계의 결과(value, done)를 담은 객체

  • 예제1: for문 만들어보기
    iterator의 next 메서드를 이용해 첫 요소를 result로 뽑는다. result.done이 false인 동안 for문 안의 코드를 실행시키고, 모든 코드가 실행되면 iterator.next()를 통해 다음 값을 result에 담는다.
let iterable = [99,88];
let iterator = iterable[Symbol.iterator]();
for(let result = iterator.next(); !result.done; result = iterator.next()){
	console.log(result.value);
}

// 99
// 88
  • 예제2: '부분적으로 사용된' 이터레이터 순회
// 임의의 배열 list
let list = [1, 2, 3];

// list의 이터레이터 객체 만들기
let iter = list[Symbol.iterator]();

// 이터레이터 객체로 첫번째 요소 순회
let head = iter.next(); // {value: 1, done: false}

// 이터레이터 객체에 남은 요소들 넣기
let tail = [...iter]; // 2, 3

12.2 이터러블 객체 만들기

어떤 형태로든 순회할 수 있는 데이터 타입이라면 이터러블로 만드는 것을 고려해 봐야 한다.

이터러블 객체와 이터리에터는 본질적으로 LAZY하다.

클래스를 이터러블로 만들어보자

  1. Symbol.iterator 메서드 만들기
  2. next() 메서드가 있는 이터레이터 객체를 반환해야 한다
  3. next() 메서드는 순회 결과 객체를 반환해야 한다.
  4. 순회 결과 객체의 value, doen 프로퍼티 중 하나는 반드시 존재해야 한다.
  • 예제 12-1 참고

이터러블의 LAZY한 특성: 다음 값을 얻기 전 필요할 때 까지 계산을 늦출 수 있다.
- 예 : 문자열을 공백으로 구분된 단어로 토큰화할 때, 첫번째 단어만 사용한다고 치자. split() 메서드를 사용한다면 문자열 전체를 처리한 후 첫번째 단어를 뽑아야겠지만, 이터레이터를 활용하면 훨씬 더 쉽게 처리할 수 있다. (ES2020 matchAll() )

12.2.1 이터레이터'종료': return 메서드

이터레이터는 반드시 끝까지 순회하지는 않는다. 필요한 처리가 끝나면 중간에 (대부분 break 등으로) 일찍 끝낼 수 있기 때문이다.

인터프리터는 이터레이터 객체에 return() 메서드가 있는지 확인하고 존재한다면 인자 없이 return() 메서드를 호출하여 파일을 닫고 메모리를 반환하는 등의 정리 작업을 한다.

return() 메서드는 반드시 순회 결과 객체를 반환해야 한다. 이때 객체의 프로퍼티는 무시되지만 객체가 아닌 값을 반환하면 에러가 일어난다.

API를 만들 때 커스텀 이터레이터가 매우 유용하며, 제너레이터를 사용해 이를 쉽게 만들 수 있기 때문에 다음 장에서는 제너레이터를 자세히 살펴본다.

12.3 제너레이터

ES6 문법(yield)을 사용한 일종의 이터레이터
계산 결과를 순회할 때 특히 유용하다.

function* 키워드

함수 바디를 실행시키지 않고 제너레이터 객체(이터레이터)를 반환한다.

제너레이터 객체의 next()메서드를 호출하면 함수 바디 처음 또는 현재 위치에서 실행되며 yield문을 만나면 멈춘다.

function* oneDigitPrimes() {

  yield 2;
  yield 3;
  yield 5;
  yield 7;

}

  

let primes = oneDigitPrimes();

  
const a = primes.next();
const b = primes.next();
const c = primes.next();
const d = primes.next();
const e = primes.next();

console.log(a, b, c, d, e);
// { value: 2, done: false } 
// { value: 3, done: false } 
// { value: 5, done: false } 
// { value: 7, done: false } 
// { value: undefined, done: true }

특징 1.

다른 이터러블 타입처럼 사용할 수 있다.

[...oneDigitPrimes()] // [2,3,5,7]

let sum = 0;
for(let prime of oneDigitPrimes()) sum += prime;
sum // 17

특징 2.

표현식 형태로 정의하거나 클래스와 객체 리터럴에서 단축 프로퍼티 표기법을 쓸 수 있다.

let o = {

  x: 1,
  y: 2,
  z: 3,

  *g() {
    for (let key of Object.keys(this)) {
      yield key;
    }
  },

};

  

console.log([...o.g()]);
// [ 'x', 'y', 'z', 'g' ]

특징 3.

화살표 함수 문법으로 정의할 수 없다.

이터러블 클래스를 제너레이터를 이용해 줄여보자


// 제너레이터를 이용해 줄어든 코드 

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


// 12-1 제너레이터 없이 만든 이터러블

  [Symbol.iterator]() {
    let next = Math.ceil(this.from); // 반환할 다음 값입니다.
    let last = this.to; // to를 초과하는 값은 반환하지 않습니다.

    return {
    
      next() {
        return next <= last // 마지막 값을 아직 반환하지 않았다면
          ? { value: next++ }
          : { done: true }; // 그렇지 않다면 마지막 값을 반환했다고 알립니다.
      },

      [Symbol.iterator]() {
        return this;
      },
    };
  }

12.3.1 제너레이터 예제

계산을 통해 전달할 값을 실제로 '생성'할 때 더 유용하다.

  • 피보나치 수열 제너레이터 함수

12.3.2 yield* 와 재귀 제너레이터

이터러블 객체를 순회하면서 각각의 값을 전달 (cf. yield는 값 하나를 전달)

function* sequence(...iterables) {
  for (let iterable of iterables) {
  
	// yield*로 축약이 가능한 부분
    for (let i of iterable) {
      yield i;
    }
  }
}


function* sequence2(...iterables) {
  for (let iterable of iterables) {
    yield* iterable;
  }
}

  

console.log([...sequence('abc', '12')]); // [ 'a', 'b', 'c', '1', '2' ]
console.log([...sequence2('abc', '12')]); // [ 'a', 'b', 'c', '1', '2' ]

!주의!
yield와 yield* 키워드는 제너레이터 함수 안에서만 사용할 수 있다.

function* sequence(...iterables) {
  // 중첩된 화살표 함수는 일반적인 함수이므로 yield가 허용되지 않는다.
  iterables.forEach(iterable => yield * iterable);
}

console.log([...sequence('abc', '12')]); 
// ReferenceError: yield is not defined

yield* 는 어떤 이터러블 객체와도 함께 사용할 수 있다. 이를 활용해 재귀 제너레이터를 만들 수 있고, 재귀적으로 정의된 트리 구조에 비재귀적 순회를 수핼할 수 있다.

12.4 고급 제너레이터 기능

계산을 잠시 멈추고 중간 값을 전달한 다음 계산을 재개할 수 있는 특징

12.4.1 제너레이터 함수의 반환 값

순회 마지막 요소의 반환 값은 일반적인 순회에서는 나타나지 않는다.
단, next()를 명시적으로 호출한다면 사용할 수 있다.

function* oneAndDone() {

  yield 1;
  return 'done';

}


console.log(...oneAndDone()); // 1


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

// 순회가 다 돌았기 때문에 done 에 true값이 있고, value 에 반환값이 담겨 있다.
console.log(generator.next()); // { value: 'done', done: true }
console.log(generator.next()); // { value: undefined, done: true }

12.4.2 yield 표현식의 값

yield 키워드에 있는 표현식을 평가한 값이 next()의 반환 값이다. 이 시점에서 제너레이터 함수 실행을 즉시 멈춘다.

즉, 제너레이터는 yield로 호출자에게 값을 반환하며 호출자는 next()를 통해 제너레이터에 값을 전달한다.

제너레이터와 호출자는 값과 실행 권한을 주고받는 별도의 실행 스트림이다.

next()를 첫 번째로 호출할 때 전달된 값은 제너레이터에서 접근할 수 없다.

function* smallNumbers() {
  console.log('next() 첫 호출, 인자는 무시됨');
  
  let y1 = yield 1;
  console.log(`next() 2번째 호출, 인자는 ${y1}`);
  
  let y2 = yield 2;
  console.log(`next() 3번째 호출, 인자는 ${y2}`);
  
  return 3;
}

  

let g = smallNumbers();
let n1 = g.next('a');
let n2 = g.next('b');
let n3 = g.next('c');
let n4 = g.next('d');

console.log(n1, n2, n3, n4);

/**
next() 첫 호출, 인자는 무시됨
next() 2번째 호출, 인자는 b
next() 3번째 호출, 인자는 c

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

12.4.3 제너레이터의 return()가 throw() 메서드

return() : 값 반환
throw() : 예외 발생
yield* 를 이용해 이터레이터의 return, throw를 실행시킨다.

이터레이터의 return 메서드처럼 인터프리터가 자동으로 파일을 닫거나 메모리 정리 작업을 수행하게 할 수는 없지만, try finally문을 통해 제너레이터 종료 시 필요한 정리 작업을 수행하게 만들 수 있다.

제너레이터가 yield* 를 통해 다른 이터러블 객체에 값을 전달하면 제너레이터의 next()가 호출될 때 이터레이터의 next() 도 호출되는 것을 이용해 return, throw 메서드도 호출시킬 수 있다.

이터레이터는 next()가 반드시 있어야 하고 불완전하게 순회를 끝낼 때를 대비해 return()메서드를 정의해야 한다.

실용적인지는 의문이지만 이터레이터는 throw()메서드를 정의할 수 있다.

12.4.4 제너레이터에 대한 마지막 노트

제너레이터로 프로그램의 비동기 부분을 감싸 함수 일부분이 실제로는 비동기이지만 프로그램 전체는 절차적이고 동기적으로 보이게 만들 수도 있다.

실전에서는 asyncawait 키워드를 사용해 이를 처리할 수 있다.

12.5 요약

  • for/of 루프와 분해 연산자 ... 는 이터러블 객체와 함께 사용할 수 있다.

  • 이터레이터 객체를 반환하는, 심벌 이름 [Symbol.iterator] 메서드를 가진 객체는 이터러블이다.

  • 이터레이터 객체에는 순회 결과 객체를 반환하는 next() 메서드가 있다.

  • 순회 결과 객체 = {value, done}

  • 다음 순회 값이 있으면 value가 값을 가지고 있고, 순회가 완료되었다면 done 이 true다.

  • 이터러블 객체를 직접 만들 수 있다.

  • function* 로 정의된 제너레이터 함수로도 이터레이터를 만들 수 있다.
    - 제너레이터 함수를 호출하면 함수 바디가 즉시 실행되지는 않고, 반환 값은 이터러블인 이터레이터 객체다.
    - 이터레이터 객체에 next() 메서드를 호출할 때마다 제너레이터 함수의 다른 코드 덩어리가 실행된다. (다음 yield를 만날 때 까)
    - 이터레이터가 반환하는 값은 yield 표현식의 값이다.
    - yield 표현식이 더 없으면 제너레이터 함수는 실행을 종료하고 순회도 완료된다.

note

  • Unit8Array
    - 8비트 정수로 타입 정의된 배열
    - 배열 메서드 대부분 사용할 수 있는 것 같다. (of, from, some, filter ...)
    - MDN Unit8Array
profile
무럭무럭 자라는 주니어 프론트엔드 개발자입니다.

0개의 댓글