Iterator와 Generator

woolee의 기록보관소·2022년 12월 19일
0

FE개념정리

목록 보기
29/35

UI에 데이터를 바인딩하기 위해서는 반복(Iterate)이 필요하다.

자바스크립트는 collection에 대한 iterating하는 다양한 방법(간단한 for loops부터, map(), filter() 등등.)을 제공한다.

이때 Iterators와 Generators는 반복의 개념을 자바스크립트로 가져오고, for...loop를 customizing하는 메커니즘을 제공하는 역할을 한다. Iterators와 generators은 ES6 자바스크립트에서 데이터를 반복하는 쉬운 방법이다.

Iterators

iteration에서 핵심 개념은 2가지이다.

  1. iterable은 public하게 접근 가능한 자료구조이다. Symbol.iterator를 사용하면 쉽게 iterable을 만들 수 있다.
  2. iterator은 자료구조의 요소를 순회하기 위한 pointer이다.

자바스크립트에서 반복 가능한 values들은 다음과 같다.

  • Arrays, Strings, Maps, Sets, DOM data structures

일반 객체는 반복할 수 없다.

배열과 객체를 비교해보면,

배열은 index와 element로 구성되는 반면, 객체는 key와 value로 구성된다(배열은 index를 참조하고 객체는 key 값을 참조한다).
배열은 순서(order)와 길이 프로퍼티(length property)를 갖지만, 객체는 그렇지 않다.

자바스크립트는 기본적으로 밀집 배열이 아니라 희소 배열이다. 데이터들이 메모리 상으로 밀집해 있지 않으며, 접근하는 데 있어 속도가 느리다. 물론 요소를 삽입하거나 삭제할 때는 빠르다.

iterability

반복 개념에서는 다음의 두 가지를 알고 있으면 좋다.

  1. Data consumers : 자바스크립트는 데이터를 소비하는 구조이다. 예를 들어, for..of는 값에 대해 순회하고, 전개 연산자 ...는 값을 배열이나 함수 호출 시에 삽입한다.
  2. Data sources : data consumers는 다양한 sources로부터 값들을 가져올 수 있다. 예를 들어 다양한 sources(배열의 요소, Map의 key-value, 문자열의 문자)를 반복할 수 있다.

ES6 이전에는 배열, 문자열, DOM 컬렉션 등이 각자의 방법으로 데이터를 반복했다면, ES6에서는 이러한 반복 가능한 데이터들이 이터레이션 프로토콜을 준수해서 동일하게 동작하게끔 만들었다(ES6에서는 Interface Iterable를 도입했다).

Symbol.iterator 메서드를 사용하면 이터러블 객체를 만들 수 있다(일반 객체도 이터러블 객체로 만들 수 있다).

Symbols은 고유하고 다른 속성과 충돌할 수 없는 이름을 제공한다.
Symbol.iteratoriterator라는 객체를 반환하며, 이 iterator는 next() 메서드를 갖는다.

ES6에서
Destructuring via an Array pattern, for-of loop, Array.from(), Spread operator (...), Constructors of Maps and Sets, Promise.all(), Promise.race(), yield*
들은 모두 Iterable을 사용한다.

iterable 객체

iterable 객체는 배열을 일반화한 객체이다. 원래 일반 객체는 반복할 수가 없다. order와 length property가 없기 때문이다.

ES6에 와서 iterable 개념을 사용하면 어떤 객체든 for..of 반복문을 적용할 수 있다. 즉, 어떤 객체든 반복할 수 있다.

일반 객체를 만들려면 Symbol.iterator 메서드를 사용해서 아래와 같은 과정을 밟도록 하면 된다.

  1. for..of가 시작하자마자 for..of는 Symbol.iterator를 호출한다. 그리고 Symbol.iterator는 반드시 iterator(next 메서드가 있는 객체)를 반환해야 한다.
  2. 이후 for..of는 반환된 객체(iterator)만 대상으로 동작한다.
  3. for..of에 다음 값이 필요하면 for..of는 이터레이터(iterator)의 next() 메서드를 호출한다.
  4. next()의 반환 값은 {done: Boolean, value: any} 형태여야 한다. done:true이면 반복이 종료되었다는 의미이다. done:false일 때는 value에 다음 값이 저장된다.
let range = {
  from: 1,
  to: 5
};
// 1. for..of 최초 호출 시, Symbol.iterator가 호출됩니다.
range[Symbol.iterator] = function() {
  // Symbol.iterator는 이터레이터 객체를 반환합니다.
  // 2. 이후 for..of는 반환된 이터레이터 객체만을 대상으로 동작하는데, 이때 다음 값도 정해집니다.
  return {
    current: this.from,
    last: this.to,
    // 3. for..of 반복문에 의해 반복마다 next()가 호출됩니다.
    next() {
      // 4. next()는 값을 객체 {done:.., value :...}형태로 반환해야 합니다.
      if (this.current <= this.last) {
        return { done: false, value: this.current++ };
      } else {
        return { done: true };
      }
    }
  };
};
// 이제 의도한 대로 동작합니다!
for (let num of range) {
  alert(num); // 1, then 2, 3, 4, 5
}

이터러블 객체의 핵심은 '관심사의 분리(Separation of concern, SoC)'에 있다.
=> 일반 객체에는 next()가 없으므로 대신 일반객체에서 Symbol.iterator를 호출한다. 일반객체[Symbol.iterator]() 호출해서 만든 iterator 객체와 이 객체의 next() 메서드에서 iteration에 사용될 값을 만들어내는 것이다.

문자열도 이러터블 객체이다.
그러므로 문자열도 for..of로 반복할 수 있다.

명시적으로 iterator를 호출하기

for..of를 사용하지 않고 수동으로 iterator를 호출할 수도 있다.

let str = "Hello";

// for..of를 사용한 것과 동일한 작업을 합니다.
// for (let char of str) alert(char);

let iterator = str[Symbol.iterator]();

while (true) {
  let result = iterator.next();
  if (result.done) break;
  alert(result.value); // 글자가 하나씩 출력됩니다.
}

명시적으로 호출할 일은 거의 없지만,
이 방법을 사용하면 for..of를 사용하는 것보다 반복 과정을 더 잘 통제할 수 있다. 반복 중에 잠시 멈추고 다른 작업을 하다가 반복 작업을 할 수도 있다.

이터러블과 유사 배열

  • 이터러블(iterable) 은 위에서 설명한 바와 같이 메서드 Symbol.iterator가 구현된 객체이다.
  • 유사 배열(array-like) 은 인덱스와 length 프로퍼티가 있어서 배열처럼 보이는 객체이다.

문자열은 이터러블 객체이자 유사배열 객체이다.

이터러블 객체이지만, index나 length propery가 없으면 유사배열 객체가 아니다.

이터러블 객체와 유사 배열 객체는 보통은 배열이 아니므로 pop, push 같은 배열 메서드를 사용할 수 없다.

이터러블 객체와 유사 배열 객체에서 배열 메서드를 사용하려면?

  • 범용 메서드 Array.from : 이터러블 객체나 유사배열 객체를 받아 진짜 배열을 만들어 준다.
const Arr = Array.from({length: 10}, () => 0);
  • 혹은 전개 연산자를 사용해 배열을 만들 수도 있다.
const Arr = [...searchWrapEl.querySelectorAll('.className')]

Generators

제너레이터는 특정 코드를 실행하는 동안, 멈추거나 재개할 수 있는 일종의 프로세스이다.

function* func() {
    // (A)
    console.log('First');
    yield;
    console.log('Second');
}

function*는 generator functions을 위한(관련 메서드를 사용할 수 있는) 키워드이다.

yield는 generator 스스로 멈출 수 있도록 하는 연산자이다. 그리고 yield를 통해 generator는 input을 받거나 output을 내보낼 수도 있다.

function* increment(i) {
    yield i + 1;
    yield i + 2;
}
var obj = increment(10);
console.log(obj.next()); \\{value: 11, done: false}
console.log(obj.next()); \\{value: 12, done: false}
console.log(obj.next()); \\{value: undefined, done: true}

Generators 사용

일반 함수는 하나의 값(혹은 0개의 값)만을 반환한다.

하지만 제너레이터를 사용하면 여러 개의 값을 필요에 따라 하나씩 반환(yield)할 수 있다. 제너레이터와 이터러블 객체를 함께 사용하면 손쉽게 데이터 스트림을 만들 수 있다.

다양한 정의 방법

  1. Generator 함수 선언
function* genFunc() { ··· }
const genObj = genFunc();
  1. Generator 함수 표현식
const genFunc = function* () { ··· };
const genObj = genFunc();
  1. 객체 리터럴에서의 Generator 메서드 정의
const obj = {
     * generatorMethod() {
         ···
     }
};
const genObj = obj.generatorMethod();
  1. class 정의에서의 Generator 메서드 정의
class MyClass {
     * generatorMethod() {
         ···
     }
 }
 const myInst = new MyClass();
 const genObj = myInst.generatorMethod();

제너레이터 동작 방식

제너레이터는 일반 함수와 동작 방식이 다르다.
제너레이터 함수를 호출하면 코드가 실행되지 않고, 대신 실행을 처리하는 특별 객체인 "제너레이터 객체"가 반환된다.

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

// '제너레이터 함수'는 '제너레이터 객체'를 생성합니다.
let generator = generateSequence();
alert(generator); // [object Generator]

위와 같이 코드를 작성하면
제너레이터 함수는 아직 실행되지 않은 상태이다.
next()는 제너레이터의 주요 메서드인데, next()를 호출하면 가장 가까운 yield <value> 문을 만날 때까지 실행이 지속된다(value를 생략할 수 있으며, 이 경우 undefined가 된다).

이후 yield <value> 문을 만나면 실행을 멈추고 산출하고자 하는 값인 <value>가 바깥 코드로 반환된다.

next() 메서드는 항상 두 프로퍼티를 갖는 객체를 반환한다.

  • value : 산출값,
  • done : 함수 코드 실행이 끝났으면 true, 아니면 false
function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}

let generator = generateSequence();

let one = generator.next();
alert(JSON.stringify(one)); // {value: 1, done: false}
// 함수 실행이 진행 중이며 아직 yield 1에 멈춰 있다.

let two = generator.next();
alert(JSON.stringify(two)); // {value: 2, done: false}
// 함수 실행이 진행 중이며 아직 yield 2에 멈춰 있다.

et three = generator.next();
alert(JSON.stringify(three)); // {value: 3, done: true}
// 함수 실행이 진행 중이며 아직 yield 3에 멈춰 있다.

done: true인 순간 제너레이터가 종료된다. 이후에는 generator.next()를 호출해도 {done: true} 객체만을 반환할 뿐이다.

제너레이터와 이터러블

제너레이터는 이터러블이다.
즉, for..of 반복문을 사용해 값을 얻을 수 있다.

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

let generator = generateSequence();

for(let value of generator) {
  alert(value); // 1, 2, 3
}

주의할 점으로는 일반 함수에서 return으로 반환하는 것과 달리 제너레이터 함수에서는 yield로 마무리해야 한다. 이유는 for..of 이터레이션이 done: true일 때 마지막 value를 무시하기 때문이다.

제너레이터는 이터러블 객체이므로 전개 문법도 가능하다.

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

let sequence = [0, ...generateSequence()];

alert(sequence); // 0, 1, 2, 3

제너레이터 컴포지션(generator composition)

제너레이터 컴포지션은 제너레이터 안에 제너레이터를 embedding 또는composing할 수 있는 제너레이터의 특별한 기능이다.

제너레이터 특수 문법인 yield*를 사용하면 제너레이터를 다른 제너레이터에 끼워넣을 수 있다.

yield* 지시자는 실행을 다른 제너레이터에 위임(delegate)한다.

제너레이터 컴포지션을 사용하면 한 제너레이터의 흐름을 자연스럽게 다른 제너레이터에 삽입할 수 있다. 제너레이터 컴포지션을 사용하면 중간 결과 저장 용도의 추가 메모리가 필요하지 않다.

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

function* generatePasswordCodes() {

  // 0..9
  yield* generateSequence(48, 57);
  // for (let i = 48; i <= 57; i++) yield i; 와 같다.

  // A..Z
  yield* generateSequence(65, 90);
  // for (let i = 65; i <= 90; i++) yield i; 와 같다.

  // a..z
  yield* generateSequence(97, 122);
  // for (let i = 97; i <= 122; i++) yield i; 와 같다. 

}

let str = '';

for(let code of generatePasswordCodes()) {
  str += String.fromCharCode(code);
}

alert(str); // 0..9A..Za..z

제너레이터를 사용하면 이터레이터를 더 쉽게 쓸 수 있다.

제너레이터는 이터레이터를 어떻게 하면 쉽게 구현할지를 염두에 두고 자바스크립트에 추가되었다.

let range = {
  from: 1,
  to: 5
};
range[Symbol.iterator] = function() {
  return {
    current: this.from,
    last: this.to,
    next() {
      if (this.current <= this.last) {
        return { done: false, value: this.current++ };
      } else {
        return { done: true };
      }
    }
  };
};
for (let num of range) {
  alert(num); // 1, then 2, 3, 4, 5
}

이터레이터를 사용한 위 예시를 제너레이터를 사용하면 더 간결하게 코드를 작성할 수 있다.

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

  *[Symbol.iterator]() { // [Symbol.iterator]: function*()를 짧게 줄임
    for(let value = this.from; value <= this.to; value++) {
      yield value;
    }
  }
};

alert( [...range] ); // 1, 2, 3, 4, 5

제너레이터가 이터레이터보다 더 강력하고 유연한 이유는 yield에 있다.
yield는 양방향 길과 같은 역할을 한다. yield는 결과를 바깥으로 전달할 뿐만 아니라 값을 제너레이터 안으로 들여오기도 한다.

값을 안, 밖으로 전달하려면 generator.next(arg)를 호출하면 된다. 이떄 인수 arg는 yield의 결과가 된다.

function* gen() {
  // 질문을 제너레이터 밖 코드에 던지고 답을 기다립니다.
  let result = yield "2 + 2 = ?"; // (*)

  alert(result);
}

let generator = gen();

let question = generator.next().value; // <-- yield는 value를 반환합니다.

generator.next(4); // --> 결과를 제너레이터 안으로 전달합니다.
  1. generator.next()를 처음 호출할 때는 항상 인수가 없어야 한다. generator.next()를 호출하면 실행이 시작되고 첫번째 yield "2+2=?"의 결과가 반환된다. 그리고 이 시점에서 (*) 줄에서 실행을 잠시 멈춘다.
  2. 그 후, yield의 결과가 제너레이터를 호출하는 외부 코드에 있는 변수인 question에 할당된다.
  3. 마지막 줄, generator.next(4)에서 제너레이터가 다시 시작되고, 4는 result에 할당된다(let result = 4 => alert창에 결과값인 4가 뜰 것이다.).

즉, 처음 next()를 제외한 이후 next(value)는 현재 yield의 결과 값을 제너레이터로 전달한다. 일반 함수와 다르게 제너레이터와 외부 호출 코드는 next/yield를 이용해 결과를 주고 받는다.

generator.throw

외부 코드가 yield의 결과가 될 값을 제너레이터 안에 전달하기도 한다고 했는데, 외부 코드가 에러를 만들거나 던질 수도 있다. 에러는 결과의 한 종류일 뿐이므로 자연스러운 현상이다.

에러를 yield 안으로 전달하려면 generator.throw(err)를 호출해야 한다.

function* gen() {
  try {
    let result = yield "2 + 2 = ?"; // (1)

    alert("위에서 에러가 던져졌기 때문에 실행 흐름은 여기까지 다다르지 못합니다.");
  } catch(e) {
    alert(e); // 에러 출력
  }
}

let generator = gen();

let question = generator.next().value;

generator.throw(new Error("데이터베이스에서 답을 찾지 못했습니다.")); // (2)

async 이터레이터와 제너레이터

비동기 이터레이터(asynchronous iterator)를 사용하면 비동기적으로 들어오는 데이터를 필요에 따라 처리할 수 있다. 덕분에 네트워크를 통해 데이터가 여러 번에 걸쳐 들어오는 상황을 처리할 수 있다. 비동기 이터레이터에 더해 비동기 제너레이터(asynchronous generator)를 사용하면 이런 데이터를 좀 더 편리하게 처리할 수 있다.

async 이터레이터

비동기 이터레이터는 약간의 문법 차이를 제외하고는 일반 이터레이터와 유사하다.

이터러블 객체를 비동기적으로 만들려면
1. Symbol.iterator 대신, Symbol.asyncIterator를 사용해야 한다.
2. next()promise를 반환해야 한다.
3. 비동기 이터러블 객체를 대상으로 하는 반복 작업은 for await (let item of iterable) 반복문을 사용해 처리해야 한다.

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

  // for await..of 최초 실행 시, Symbol.asyncIterator가 호출됩니다.
  [Symbol.asyncIterator]() { // (1)
    // Symbol.asyncIterator 메서드는 이터레이터 객체를 반환합니다.
    // 이후 for await..of는 반환된 이터레이터 객체만을 대상으로 동작하는데,
    // 다음 값은 next()에서 정해집니다.
    return {
      current: this.from,
      last: this.to,

      // for await..of 반복문에 의해 각 이터레이션마다 next()가 호출됩니다.
      async next() { // (2)
        //  next()는 객체 형태의 값, {done:.., value :...}를 반환합니다.
        // (객체는 async에 의해 자동으로 프라미스로 감싸집니다.)

        // 비동기로 무언가를 하기 위해 await를 사용할 수 있습니다.
        await new Promise(resolve => setTimeout(resolve, 1000)); // (3)

        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      }
    };
  }
};

(async () => {

  for await (let value of range) { // (4)
    alert(value); // 1,2,3,4,5
  }

})()

이터레이터와 async 이터레이터 비교

이터레이터
1. 이터레이터를 제공해주는 메서드 : Symbol.iterator
2. next()가 반환하는 값 : 모든 값
3. 반복 작업을 위해 사용하는 반복문 for..of

async 이터레이터
1. 이터레이터를 제공해주는 메서드 : Symbol.asyncIterator
2. next()가 반환하는 값 : Promise
3. 반복 작업을 위해 사용하는 반복문 : for await..of

주의할 점은, 전개 문법(...)은 비동기적으로 동작하지 않는다는 점이다.
일반적인 동기 이터레이터는 비동기 이터레이터와 같이 사용할 수 없다. 전개문법(...)은 await이 없는 for..of와 마찬가지로, Symbol.asyncIterator가 아닌 Symbol.iterator를 찾기 때문에 에러가 발생한다.

async 제너레이터

일반 제너레이터에서는 await을 사용할 수 없다.
그리고 모든 값은 동기적으로 생산된다.
일반 제너레이터는 동기적인 문법이다.

제너레이터 본문에서 await을 사용해야만 하는 상황(예를 들어 네트워크 요청)에서는 아래처럼 async를 제너레이터 함수 앞에 붙여주면 된다.

async function* generateSequence(start, end) {

  for (let i = start; i <= end; i++) {

    // await를 사용할 수 있습니다!
    await new Promise(resolve => setTimeout(resolve, 1000));

    yield i;
  }

}

(async () => {

  let generator = generateSequence(1, 5);
  for await (let value of generator) {
    alert(value); // 1, 2, 3, 4, 5
  }

})();

연습 예제

async function* fetchCommits(repo) {
  let url = `https://api.github.com/repos/${repo}/commits`;

  while (url) {
    const response = await fetch(url, { // (1)
      headers: {'User-Agent': 'Our script'}, // GitHub는 모든 요청에 user-agent헤더를 강제 합니다.
    });

    const body = await response.json(); // (2) 응답은 JSON 형태로 옵니다(커밋이 담긴 배열).

    // (3) 헤더에 담긴 다음 페이지를 나타내는 URL을 추출합니다.
    let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/);
    nextPage = nextPage?.[1];

    url = nextPage;

    for(let commit of body) { // (4) 페이지가 끝날 때까지 커밋을 하나씩 반환(yield)합니다.
      yield commit;
    }
  }
}

(async () => {

  let count = 0;

  for await (const commit of fetchCommits('javascript-tutorial/en.javascript.info')) {

    console.log(commit.author.login);

    if (++count == 100) { // 100번째 커밋에서 멈춥니다.
      break;
    }
  }

})();

Generators 용도

  1. Generators를 통해 더 간단한 비동기 코드 블록을 실행할 수 있다. 위에서 이터레이터로 작성한 코드를 제너레이터로 간결하게 작성한 코드를 참고하면 된다.

  2. ES8에서는 내부적으로 Generators를 기반으로 하는 비동기 함수(async/await)를 가질 수 있다.

async/await은 generators를 통해 만들어졌다.
generators는 iterator를 통해 만들어졌다(iterator를 쉽게 사용하기 위해 구현됨).

async function fetchJson(url) {
    try {
        let request = await fetch(url);
        let text = await request.text();
        return JSON.parse(text);
    }
    catch (error) {
        console.log(`ERROR: ${error.stack}`);
    }
}
const fetch = require('node-fetch')

getTitle (function* generator() {
  const url = 'http://jsonplaceholder.typicode.com/posts/1'
  const response = yield fetch(url)
  const post = yield response.json()
  const title = post.title
  console.log("Title : ", title)
})

function getTitle(generator) {
  const iterator = generator()
  const iteration = iterator.next()
  const promise = iteration.value

  promise.then(x => {
    const anotherIterator = iterator.next(x)
    const anotherPromise = anotherIterator.value

          anotherPromise.then(y => {
            iterator.next(y)
          })
  })
}
  1. Iterable 구현 (Object.entries()는 내부적으로 generators를 사용한다)

Object.entries()는 generators를 통해 만들어졌다.

객체를 반복하려면 Object.entries()를 사용할 수 있다.
Object.entries()는 내부적으로 generators를 사용한다. generators 없이 해당 메서드를 구현하려면 더 많은 작업을 해야 한다.

function* objectEntries(obj) {
    const propKeys = Reflect.ownKeys(obj);

    for (const propKey of propKeys) {
        // `yield` returns a value and then pauses
        // the generator. Later, execution continues
        // where it was previously paused.
        yield [propKey, obj[propKey]];
    }
}

const user= { name: 'raja', age: 25};
for (const [key,value] of objectEntries(user)) {
    console.log(`${key}: ${value}`);
}
// Output:
// name: raja
// age: 25

결론 (아직 정리중..)

객체 내에서 iteration logic을 유지하는 것은 좋은 습관이며, 이게 ES6 features의 초점이기도 하다.

iterator는 자바스크립트 내에서 수많은 작업을 훌륭하고 효율적이게 만들어준다. 기존에는 개별적으로 iteration을 수행했다면, iterator를 통해 for..of로 iteration을 통일할 수 있게 되었다. 일반 객체조차도 iterator를 통해서라면 이터러블 객체가 될 수 있다.

제너레이터는 이터레이터의 특별한 기능이자, 이터레이터를 더 훌륭하고 유연하게 사용할 수 있게 해준다. 제너레이터는 특히 모든 종류의 비동기 동작과 관련해서 더 깨끗한 코드를 가질 수 있게 해주는 강력한 도구이다.

제너레이터 의 장점
1. Lazy Evaluation
제너레이터는 값이 필요할 때까지 식의 평가를 지연시키는 평가 모델이다. 값이 필요하지 않으면 존재하지 않는다. 주문이 들어가야 한다.
2. Memory Efficient
필요한 값만 생성되므로 메모리가 효율적이다. 일반적인 함수는 모든 값을 미리 생성하고 나중에 사용해야 하는 경우를 대비해서 보관한다.

제너레이터와 이터러블 객체를 함께 사용하면 손쉽게 데이터 스트림을 만들 수 있다.

참고

Explanation about Iterators and Generators in Javascript ES6
Javascript의 Iterator와 Generator | TOAST UI - NHN Cloud

iterable 객체 - 모던 JavaScript 튜토리얼
제너레이터 - 모던 JavaScript 튜토리얼
async 이터레이터와 제너레이터 - 모던 JavaScript 튜토리얼

Implementing Iterators and Generators in JavaScript

profile
https://medium.com/@wooleejaan

0개의 댓글