kimjeongwonnabout

이터러블/이터레이터/제네레이터

자바스크립트의 이터러블과 이터레이터, 그리고 제네레이터의 동작 방식

자바스크립트 공부중 어려웠던 내용을 복습해 보려고 한다. 그 중 하나가 Iterable / Iterator / Generator 였다. 패스트캠퍼스 Nodejs 올인원 패키지에서는 제대로 설명해 주지 않아서 조금 아쉬웠다. 강좌의 분량을 생각하면 어쩔 수 없기도 했지만, 이해하는데 꽤나 시간을 많이 들였다. 물론 지금도 바로 설명하라고 하면 한참을 어버버 거릴 것 같다. 지식의 상태로 있는 내용들을 내것으로 만드는 건 반복 학습 뿐이라고 생각되기 때문에 다시 한 번 정리해 본다.

참고 자료 :
  • https://poiemaweb.com/es6-iteration-for-of
  • https://poiemaweb.com/es6-generator

Iteration Protocol

ES6에서 새로 도입된 데이터 컬렉션 객체(대표적으로 Array)를 순회하기 위한 Protocol(정의된 규칙)이다. 이 프로토콜을 준수한 객체만이 for of 문으로 순회할 수 있고 Spread문법의 피연산자가 될 수 있다. Iteration Protocol은 Iterable Protocol과 Iterator Protocol을 총칭한다.

1. 이터러블(Iterable)

Symbol.iterator를 키로 갖고있는 메소드를 가지고 있는 객체를 말한다. 별도로 Symbol.iterator를 키로 하여 메소드를 정의해 주거나 프로토타입 상속을 통해서 Symbol.iterator를 키로 갖고있는 메소드를 갖고 있다면 Iterable Protocol의 조건을 충족하여 Iterable이라고 할 수 있다. Iterable객체는 for of문으로 순회할 수 있고 Spread문법의 피연산자로 사용할 수 있다. 대표적으로 Array 객체가 있다. (ES9부터는 Object도 피연산자로 사용할 수 있다.)

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

console.log(Symbol.iterator in array); //true

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

1.1 빌트인 이터러블

ES6를 기준으로 이터러블을 지원하는 객체는 아래와 같다.

Array, String, Map, Set, TypedArray(Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array), DOM data structure(NodeList, HTMLCollection), Arguments

2. 이터레이터(Iterator)

이터러블 객체의 Symbol.iterator 메소드를 호출하면 Iterator객체를 반환한다. 반환된 Iterator객체는 next메소드를 소유하고 있으며 next메소드를 호출할 때 iterator result 객체를 반환한다면 Iterator Protocol을 충족하여 Iterator라고 할 수 있다. iterator result 객체는 value와 done 프로퍼티를 갖고 있으며, value는 현재 순회하는 값을 갖고 있고 done은 순회가 언제 끝나는지 알려준다. next메소드는 반복적을 호출되다가 모든 요소를 순회하게 되면 value프로퍼티는 undefined, done프로퍼티는 true가 되며 순회를 중단한다.

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

const iterator = array[Symbol.itrator]();
//Symbol.itrator를 호출해서 반환된 iterator를 받는다.

console.log(iterator.next()); //반복
//{ value: 1, done: false }
//{ value: 2, done: false }
//{ value: 3, done: false }
//{ value: 4, done: false }
//{ value: 5, done: false }
//{ value: undefined, done: true }

3. 이터러블 객체 순회하기

Iteration Protocol을 준수하는 객체를 순회하는 반복문/연산자가 있는데 대표적으로는 for of문이 있다. 이런 반복문/연산자들은 Iterable 객체에서 Iterator를 조작하여 iterator result를 참조해 명령을 실행한다.

3.1 for of문

for of문은 해당하는 객체의 Iterator를 통해 받는 iterator result객체의 done프로퍼티가 true가 될 때까지 value프로퍼티의 값을 순회하며 변수에 할당한다. 위의 빌트인 이터러블안의 객체는 모두 순환이 가능하기 때문에 문자열도 순환이 가능하다.

//배열
for (const i of [1, 2, 3, 4, 5]) {
  console.log(i); //1 2 3 4 5
}

//문자열
for (const i of 'abcde') {
  console.log(i); //'a' 'b' 'c' 'd' 'e'
}

4. 제네레이터(Generator)

직접 이터러블 객체를 만들 수도 있다. 먼저, Iteration Protocol에 맞춰 value와 done프로퍼티를 return하는 next메소드를 return하는 Symbol.iterator 메소드 를 직접 작성하여 객체에 할당해주는 방법도 있지만 제네레이터 함수를 이용해 더 쉽게 이터러블 객체를 생성할 수 있다.

아래 예제를 통해 제네레이터의 생성과 순회를 살펴보자.

//function* 을 통해 제네레이터 함수를 만들 수 있다. (화살표 함수는 사용할 수 없다.)
function* counter() {
  console.log('Point 1');
  yield 1;
  //next 메소드를 호출하면 yield까지의 명령이 처리된 뒤, yield에서 반환되는 값이 iterator result 객체의 value에 할당된다. 그 뒤 다음 next메소드가 호출 될 때 까지 대기한다.
  console.log('Point 2');
  yield 2;
  console.log('Point 3');
  yield 3;
  console.log('done'); //모든 순회가 끝나고 done 프로퍼티가 true가 된다.
}

const generatorObj = counter();
/* 제네레이터 함수를 호출해서 제네레이터 객체를 생성한다.
제네레이터를 통해 생성된 객체는 이터러블이면서 이터레이터이기 때문에 굳이 Symbol.iterator를 호출하지 않아도 next메소드를 통해 순회할 수 있다. */

for (const i of generatorObj) {
  console.log(i);
}
//이터러블 객체이기 때문에 for of 문으로 바로 순회가 가능하다.
//물론 이터레이터이기도 하기 때문에 next메소드를 호출하여 원하는 시점에서 호출이 가능하다.

generatorObj.next();
//next를 통해 순회하게 되면 yield로 반환된 값을 value프로퍼티의 값으로 갖는 iterator result 객체가 반환된다. 제네레이터 함수 안에 마지막 yield가 끝난 뒤에는 done프로퍼티는 true가 되며 순회가 종료된다.

위의 예제의 제네레이터 함수를 통해 이터러블이면서 이터레이터인 제네레이터 객체를 생성하고 생성된 제네레이터 객체는 이터러블 처럼 for of를 통해 순회하거나 이터레이터 처럼 next메소드를 통해 순회할 수 있다.

4.1 제네레이터에 인수를 전달하여 호출하기

제네레이터 함수는 함수이기 때문에 인자를 받아 제네레이터 객체를 생성할 수 있다. 다음과 같이 인자로 입력받은 횟수만큼 반복하는 제네레이터를 생성해보자.

function* gen(n) {
  for (let i = 0; i <= n; yield i++) console.log(`current number : ${i}, last number : ${n}`);
}

const genObj = gen(10);
//10번 순회할 수 있는 제네레이터 오브젝트를 생성

console.log(genObj.next()); //반복 호출하면 아래와 같은 콘솔을 볼 수 있다.

/*
제네레이터 함수 안에 있는 console.log가 출력
current number : 1, last number : 10
next메소드가 반환된 iterble result가 console.log를 통해 출력
{value: 0, done: false}
... 계속 순회하다가 인자로 넣은 마지막 숫자에 도달하면 아래와 같이 done이 true가 되어 순회를 종료
current number : 10, last number : 10
{value: 10, done: false}
{value: undefined, done: true}
*/

4.2 next메소드에 인수를 전달하여 제네레이터 호출하기

제네레이터는 이터레이터와는 다르게 next메소드에 인자를 전달하면 제네레이터 함수의 yield에 할당되어 작동한다. 이를 통해 좀 더 유연하게 제네레이터를 활용할 수 있다. 아래의 예제를 통해 인자가 어떻게 전달되는지 알 수 있다.

function* gen(n) {
  let res;
  res = yield n;

  console.log(res);
  res = yield res;

  console.log(res);
  res = yield res;

  console.log(res);
  yield res;
}

const genObj = gen(0); //0 으로 초기값 지정

console.log(genObj.next()); //제네레이터 함수 시작 => value:0
console.log(genObj.next(1)); //yield에 1을 전달 이후 함수 다시시작
console.log(genObj.next(2)); //반복
console.log(genObj.next(3)); //더이상 yield가 없기 때문에 종료
console.log(genObj.next(4)); //{ value: undefined, done: true }

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

*/

next메소드에 인수가 있을 경우에는 yield는 두가지 역할을 하게 된다.

  1. value값 반환
  2. next의 인수를 변수에 할당

먼저 res에 리터럴을 할당하기 위해서 자바스크립트가 yield n을 처리하게 된다. 그 과정에서 yield가 처리되면서 다음 next까지 함수가 일시정지 되게 된다. 그리고 다음 next가 호출 될 때 받은 인자를 yield가 반환하여 res변수에 할당하게 된다. 그리고 다시 다음 yield까지 실행한 뒤 일시정지 된다. 이 과정이 마지막 yield까지 반복된다.

4.3 return을 통해 제네레이터를 강제 종료

제네레이터 함수 안에서 return이 실행되는 즉시 done은 true가 되고 return에서 반환된 값을 가진 iterator result 객체가 반환된 뒤 제네레이터가 종료된다.

function* gen(n) {
  for (let i = 0 ; true ; i++) {
  if (i > 5) return 'end'
  yield i;
}

const genObj = gen();

console.log(genObj.next()) //반복
/*
{value: 0, done: false}
{value: 1, done: false}
{value: 2, done: false}
{value: 3, done: false}
{value: 4, done: false}
{value: 5, done: false} //for of 등을 통한 순회에서는 여기까지만 순회한다.
{value: 'end', done: true}
{value: undefined, done: true}
...
*/

return에서 값을 반환하면서 동신에 done이 true가 되기 때문에 for of문 들을 통한 순회에서는 그 전의 값까지만 순회한다.

4.4 비동기 처리

함수의 비동기 처리도 제네레이터를 통해 구현할 수 있지만 ES8의 async/await가 완전히 그 기능을 대체하고 있으니 이곳에 설명은 생략한다.

마치며

첫 번째 복습기록이다. 내가 이해가 안되었던 부분들을 최대한 이해할 수 있도록 풀어 썼다. 개인적으로는 next메소드에 인수를 넣어 호출할 때 어째서 인수로 넣은 값이 res에 할당된 뒤 console.log로 출력되는지 계속 고민했고, 그렇게 알게 된 내용을 좀 더 자세히 서술했다. 물론 다른 사람들은 당연하게 이해하고 넘어갈 수 있는 부분이었더라도 나는 저 부분이 쉽게 넘어가지지 않았다. 또한 나도 이렇게 복습/복기를 하면서 새로운 사실도 알게되었다. 현재는 React를 배우고 있는데 당연한 듯이 Object 객체에 Spread 연산을 쓰는 강의를 보면서 Object는 이터러블이 아닌데 어떻게 Spread연산이 동작하나 싶었는데 ES9부터 Object에서 Spread연산이 작동한다는 사실도 알게되었다.