본문 바로가기
Javascript

Javascript의 iterable, iterator, generator

by jewook3617 2021. 4. 6.

javascript에서의 iterable, iterator, generator에 대해 정리해보겠습니다.


TL;DR

  • iterable 프로토콜과 iterator 프로토콜을 만족하면 어떤 객체든지 iterable 객체, iterator 객체가 될 수 있음.
  • iterable 프로토콜 : iterator 객체를 반환하는 함수가 Symbol.iterator 프로퍼티에 들어있어야함.
  • iterable 객체 : for of 문이나 ...(spread) 연산자처럼 값들을 순회하는 연산에 사용될 수 있는 객체.
  • iterator 프로토콜 : next() 메서드를 가지고 있어야하면 next() 메서드는 value, done 프로퍼티를 가지는 객체를 반환해야함.
  • iterator 객체 : 값들을 순회할 수 있는 객체.
  • iterable 프로토콜과 iterator 프로토콜을 합쳐서 iteration 프로토콜이라고 부름.
  • generator 함수는 generator 객체를 만드는 함수.
  • function 키워드 뒤에 *을 붙여 function* 키워드로 함수를 선언하면 그 함수는 generator 함수가 됨.
  • arrow fuction으로는 generator 함수를 만들 수 없음.
  • generator 객체는 iterable 객체이면서 iterator 객체.
  • generator 객체는 generator 함수 내부에서 yield 된 값들을 순회함.
  • generator 함수와 generator 객체는 generator 객체의 next() 메서드를 이용해 양방향 통신이 가능함.

 


Iterable(이터러블)

먼저 iterable부터 살펴보겠습니다.

javascript에는 iterable 객체를 만들 수 있는 iterable 프로토콜이 존재합니다. 프로토콜이란 규칙을 의미합니다. 그 iterable 프로토콜을 만족하면 어떤 객체이든 iterable 객체가 될 수 있습니다. 그럼 iterable 객체는 어떤 객체일까요?

iterable 객체는 for of 문이나 ...(spread) 연산자처럼 값들을 순회하는 연산에 사용될 수 있는 객체를 말합니다. 

 

위 코드는 for of 문을 이용해 배열의 원소를 출력하는 코드입니다. for of 문에 배열이 사용될 수 있는 이유는 배열이 iterable 객체이기 때문입니다.

iterable 프로토콜에 따르면 iterable 객체가 되기 위해서 Symbol.iterator 프로퍼티를 가지고 있어야합니다. 그리고 Symbol.iterator의 value에는 iterator 객체를 리턴하는 함수가 있어야합니다.

{
  ...
  [Symbol.iterator]: iterator를 리턴하는 함수
  ...
}

위 조건을 만족하면 어떤 객체든지 iterable 객체로 만들 수 있습니다.

const arr = [1, 2, 3];

arr[Symbol.iterator];    // Function
arr[Symbol.iterator]();  // Array Iterator {}

배열이 iterable 객체인 이유는 iterable 프로토콜에 맞게 Symbol.iterator 프로퍼티가 정의되어 있기 때문입니다.

Iterator(이터레이터)

iterator도 iterable과 마찬가지로 iterator 프로토콜이 존재합니다. iterator 프로토콜을 따르면 어떤 객체든지 iterator 객체가 될 수 있습니다. iterator 객체는 값들을 순회할 수 있는 객체입니다. iterator 프로토콜에 따르면 iterator 객체가 되기 위해서 next() 메서드를 가지고 있어야합니다. 그리고 next() 메서드를 호출하면 아래 두 값을 반환해야합니다.

  • done : 모든 값들을 순회했으면 true, 아직 순회할 값이 남아있으면 false
  • value : 순회하면서 가져온 값. done이 true이면 value는 undefined 이거나 생략될 수 있음.

배열의 values() 메서드를 호출하면 배열의 원소를 순회할 수 있는 iterator 객체가 리턴됩니다. 아래 코드를 보며 iterator의 동작을 살펴보겠습니다.

const book = [
  "Twinkle, twinkle, little bat!",
  "How I wonder what you're at!",
  "Up above the world you fly",
  "Like a tea tray in the sky.",
  "Twinkle, twinkle, little bat!",
  "How I wonder what you're at!",
];

const it = book.values();

it.next();  // { value: "Twinkle, twinkle, little bat!", done: false }
it.next();  // { value: "How I wonder what you're at!", done: false }
it.next();  // { value: "Up above the world you fly", done: false }
it.next();  // { value: "Like a tea tray in the sky.", done: false }
it.next();  // { value: "Twinkle, twinkle, little bat!", done: false }
it.next();  // { value: "How I wonder what you're at!", done: false }
it.next();  // { value: undefined, done: true }
it.next();  // { value: undefined, done: true }
it.next();  // { value: undefined, done: true }

책의 내용이 담긴 book 배열을 선언하고 book.values() 메서드를 호출해 book 배열을 순회할 수 있는 iterator 객체를 가져왔습니다. 이 iterator 객체에 담긴 next() 메서드를 호출해 배열을 순회할 수 있습니다.

iterator 객체는 어떤 값까지 순회했는지를 기억하고 있다가 next() 메서드가 호출되면 그 다음 값을 value 프로퍼티에 넣어줍니다.

여기서 중요한 점은 끝까지 순회를 다 해도 iterator 객체의 next() 메서드는 계속해서 호출할 수 있다는 점입니다. 모든 값들을 다 순회한 iterator 객체는 다시 배열의 처음으로 돌아갈 수는 없지만 next() 메서드를 계속 호출할 수 있습니다.

보통 배열의 원소에 접근할 때는 arr[0], arr[2] 이런 식으로 index를 이용해 접근합니다. 하지만 iterator 객체의 next() 메서드를 이용하면 index를 이용하지 않아도 값들을 순회할 수 있습니다. for of 문이나 ...(spread) 연산자가 index 없이 iterable 객체를 순회할 수 있는 이유는 for of 문이나 ...(spread) 연산자가 내부적으로 iterator 객체의 next() 메서드를 이용하여 구현되기 때문입니다. 따라서 iterator 객체를 반환하는 함수를 Symbol.iterator 프로퍼티에 가지고 있는 iterable 객체만이 for of 문이나 ...(spread) 연산자에 사용될 수 있습니다.

아래 코드와 같이 iterator 객체와 while 문을 사용하면 for of 문 처럼 동작하도록 할 수 있습니다.

const arr = [1, 2, 3, 4, 5, 6, 7];
const it = arr.values();

let current = it.next();
while(!current.done) {
  console.log(current.value);
  current = it.next();
}

// 1
// 2
// 3
// 4
// 5
// 6
// 7

또 한 가지 중요한 점은 iterator 객체가 모두 독립적으로 동작한다는 점입니다.

const arr = ["apple", "banana", "grape"];

const it1 = arr.values();
const it2 = arr.values();

it1.next();  // { value: "apple", done: false }
it1.next();  // { value: "banana", done: false }

it2.next();  // { value: "apple", done: false }

같은 배열을 순회하는 iterator 객체라고 하더라도 각각 독립적으로 동작하기 때문에 위 코드처럼 it1과 it2가 따로따로 순회하게 됩니다.

지금까지 살펴본 iterable 프로토콜iterator 프로토콜을 합쳐서 iteration 프로토콜이라고 부릅니다. 이 두 프로토콜에 맞게 새로운 iterable 객체와 iterator 객체를 만들어보겠습니다.

다음은 피보나치 수열의 값들을 순회할 수 있는 iterable 객체를 만드는 코드입니다.

class FibonacciSequence {
  [Symbol.iterator]() {
    let a = 0, b = 1;
    return {
      next() {
        let val = { value: b, done: false };
        b += a;
        a = val.value;
        return val;
      }
    };
  }
}

const fib = new FibonacciSequence();

let i = 0;
for (const n of fib) {
  console.log(n);
  if (++i > 9) break;
}

// 1
// 1
// 2
// 3
// 5
// 8
// 13
// 21
// 34
// 55

FibonacciSequence 클래스에 Symbol.iterator() 메서드를 정의했고 Symbol.iterator() 메서드가 iterator 객체를 반환하기 때문에 fib 객체는 iterable 객체가 됩니다. Symbol.iterator() 메서드가 반환하는 객체를 살펴보면 next() 메서드를 가지고 있고 next() 메서드가 value와 done 프로퍼티를 가지는 객체를 리턴하기 때문에 iterator 객체가 됩니다. 

fib 객체가 iterable 객체이기 때문에 for of 문을 사용해 값들을 순회할 수 있습니다. 다만 피보나치 수열은 무한수열이라 done이 항상 false 이기 때문에 for of 문은 무한루프에 빠지게 됩니다. 무한루프에 빠지지 않도록 피보나치 수를 10번만 출력한 후 for of 문을 빠져나오도록 했습니다.

Generator(제너레이터)

이번에는 generator 라는 특별한 함수에 대해 알아보겠습니다.

generator 함수generator 객체를 만드는 함수입니다. generator 객체iterable 객체이면서 동시에 iterator 객체입니다. function 키워드 뒤에 *을 붙여 function* 키워드로 함수를 선언하면 그 함수는 generator 함수가 됩니다. arrow function 으로는 generator 함수를 선언할 수 없습니다.

function* rainbow() {
  yield "red";
  yield "orange";
  yield "yellow";
  yield "green";
  yield "blue";
  yield "indigo";
  yield "violet";
}

const it = rainbow();

it.next();  // { value: 'red', done: 'false' }
it.next();  // { value: 'orange', done: 'false' }
it.next();  // { value: 'yellow', done: 'false' }
it.next();  // { value: 'green', done: 'false' }
it.next();  // { value: 'blue', done: 'false' }
it.next();  // { value: 'indigo', done: 'false' }
it.next();  // { value: 'violet', done: 'false' }
it.next();  // { value: 'undefined', done: 'true' }
it.next();  // { value: 'undefined', done: 'true' }

generator 함수인 rainbow() 함수를 선언하고 호출하여 generator 객체를 생성했습니다. generator 객체는 iterator이기도 하기때문에 next() 메서드를 호출하여 값들을 순회할 수 있습니다. 이 때 generator 객체generator 함수 내부에서 yield 된 값들을 순회하게 됩니다.

여기서 중요한 점은 generator 함수는 함수 실행의 제어권을 generator 객체에게 넘긴다는 점입니다. 일반적인 함수는 all or nothing 방식으로 동작합니다. 함수가 끝까지 실행되거나 실행되지 않거나 둘 중 하나입니다. 함수의 중간까지만 실행하고 멈추는 경우는 없습니다.

하지만 generator 함수는 generator 객체를 이용해 단계별로 실행해나갑니다. 위 코드처럼 yield 된 값들을 전부 순회하고 함수를 종료시킬수도 있고 일부 값만 순회하고 함수의 중간까지만 실행시킬수도 있습니다.

generator 함수의 또 한 가지 중요한 점은 generator 객체와 양방향 통신을 할 수 있다는 점입니다.

function* conversation() {
  const name = yield "What is your name?";
  const age = yield "How old are you?";

  return `${name} is ${age} years old`;
}

const it = conversation();
it.next();       // { value: "What is your name?", done: false }
it.next("Jony"); // { value: "How old are you?", done: false }
it.next(2);      // { value: "Jony is 2 years old", done: true }

generator 함수인 conversation() 함수를 선언하고 호출하여 generator 객체를 생성했습니다. rainbow 예제코드와 마찬가지로 next() 메서드를 호출하여 yield 된 값들을 순회합니다. 하지만 위 코드에서는 next() 메서드에 값을 넘겨줘서 generator 함수와 통신을 하고 있습니다.

generator 객체가 첫 번째 next() 메서드가 호출하면 generator 함수는 첫 번째로 yield 된 값인 "What is your name?" 을 value 로 넘겨주줍니다. generator 객체는 두 번째 next() 메서드를 호출하면서 인자로 "Jony" 를 generator 함수로 넘겨줍니다. generator 함수는 next() 메서드를 통해 넘어온 "Jony"를 name 변수에 저장하고 두 번째로 yield 된 값인 "How old are you?" 를 value 로 넘겨줍니다. generator 객체는 세 번째 next() 메서드를 호출하면서 인자로 2를 generator 함수로 넘겨줍니다. generator 함수는 next() 메서드를 통해 넘어온 2를 age 변수에 저장하고 return 문을 value로 넘겨줍니다. 이런 식으로 generator 함수와 generator 객체는 양방향으로 데이터를 주고받으며 통신할 수 있습니다. generator 객체가 generator 함수로 넘겨준 값에 따라 generator 함수의 결과가 달라질 수 있게 됩니다.

여기서 한 가지 주의할 점이 있습니다. generator 함수에서는 마지막 yield 문이더라도 done이 false 입니다. 하지만 return 문을 사용하면 그 위치에 관계없이 done이 true가 되고 더 이상 순회하지 않습니다. 하지만 보통 done이 true이면 value 값을 신경쓰지 않기 때문에 주의해서 사용해야합니다. 예를 들어 아래 코드에서는 for of 문에서 c는 출력되지 않습니다.

function* abc() {
  yield 'a';
  yield 'b';
  return 'c';
}

const it = abc();
it.next();  // { value: 'a', done: false }
it.next();  // { value: 'b', done: false }
it.next();  // { value: 'c', done: true }

for (const value of abc()) {
  console.log(value);
}

// 'a'
// 'b'

이 글에 쓰인 코드들은 O'REILLY의 '러닝 자바스크립트' 책에 있는 코드를 참고했습니다.

'Javascript' 카테고리의 다른 글

Javascript의 callback과 promise  (0) 2021.05.19
Javascript의 실행환경과 동작방식  (0) 2021.04.25
프로토타입 체인을 이용한 Javascript의 상속  (0) 2021.02.21
Javascript의 class  (0) 2021.02.08
Javascript의 "this"  (0) 2021.01.21