상세 컨텐츠

본문 제목

[JavaScript] 이터레이터와 제너레이터

JavaScript & TypeScript

by Yoonsang's Log 2023. 1. 21. 15:47

본문

이터레이터 정의

ES6부터는 Array, Set, Map 등을 순회하기 위한 이터레이션 프로토콜(iteration protocol)이라는 것이 도입되었다.

아래 예제 코드를 살펴보면,

const array = [1,2,3]
for(let i = 0; i < 3; i++) {
	console.log(array[i]);     // => 1,2,3
}

const set = new Set([1,2,3])
for(let i = 0; i < 3; i++) {
	console.log(set[i]);     // => undefined, undefined, undefined
}

const map = new Map(['a', 1], ['b', 2], ['c',3])
for(let i = 0; i < 3; i++) {
	console.log(set[i]);     
}
// => Uncaught TypeError: Iterator value a is not an entry object

Array의 경우 key 값을 통해 값에 접근할 수 있으나, Set와 Map의 경우 undefined를 출력하거나 TypeError가 발생한다.

Set와 Map은 Array와 달리 Key값을 통해 순회하는 것이 아닌, 다른 방식으로 동작한다는 것을 의미한다.

이터러블

이터레이션 프로토콜에는 이터러블 프로토콜(iterable protocol)과 이터레이터 프로토콜(iterator protocol)이 있다.

이터러블 프로토콜을 준수하는 객체를 이터러블이라고 하는데,

이터러블 객체는 Symbol.iterator() 메소드를 프로퍼티 키로 사용한 메소드를 직접 구현하거나 프로토 타입 체인을 통해 상속받은 객체를 의미한다.

흔히 사용하는 …(스프레드)연산자나 배열 디스트럭처링 할당 등은 이터러블 객체를 분해해서 사용하는 것이다.

// 스프레드 문법
const chars = [..."abcd"];    
console.log(chars);     // => ["a", "b", "c", "d"] 문자열도 이터러블

// 배열 디스트럭처링 할당
const [one, two, three, four] = chars;
console.log(one, two);     // => a b

이터레이터

이터러블의 [Symbol.iterator]() 메소드를 호출하면 이터레이터 프로토콜을 준수한 이터레이터를 반환한다.

반환한 이터레이터 객체는 next() 메소드를 가지고 있으며, 이를 이터레이터 결과(iterator result) 객체라고 한다.

이터레이터 결과 객체는 value와 done 프로퍼티를 통해 값과 순회가 완료되었는지 여부를 판단한다.

const Array = [1,2,3];
const iterator = Array[Symbol.iterator]();

console.log(iterator.next())     // => { value: 1, done: false }
console.log(iterator.next())     // => { value: 2, done: false }
console.log(iterator.next())     // => { value: 3, done: false }
console.log(iterator.next())     // => { value: undefined, done: true }

위와 같이 마지막까지 순회했을 경우, { value: undefined, done: true } 를 출력하는 것을 볼 수 있다.

즉, 자바스크립트에서 이터러블 객체를 순회하는 for of 루프나 …(스프레드)연산자 등은

next() 메소드를 반복적으로 호출하며 반환값의 done 프로퍼티가 true일 때까지 반복한다.

이터러블 객체 만들기

앞서 이터러블 객체는 [Symbol.iterator]() 메소드를 소유하고 있다.

일반 객체의 경우 [Symbol.iterator]() 메소드를 소유하고 있지 않기 때문에 이터러블이 아니며, for of 문이나 …(스프레드)연산자로 순회할 수 없다.

const obj = { a: 1, b: 2 };

for(const a of obj) {
	console.log(a)
}
// => Uncaught TypeError: obj is not iterable

즉, 이터러블 객체를 만들기 위해서는 아래 4가지를 준수해야 한다.

  • 반드시 이름이 [Symbol.iterator]() 인 메소드를 만들어야 한다.
  • [Symbol.iterator]() 메소드는 next() 메소드가 있는 이터레이터 객체를 반환해야 한다.
  • next() 메소드는 반드시 이터레이터 결과 객체를 반환해야 한다.
  • 이터레이터 결과 객체에는 value 프로퍼티와 done 프로퍼티 중 하나는 반드시 존재해야 한다.

커스텀 이터러블 객체

우선 이름이 [Symbol.iterator]() 인 메소드를 만들어, next() 메소드를 반환하는 이터러블 객체를 만들어 보자.

아래 코드는 value 프로퍼티가 1부터 시작해서 1씩 증가하며 두자리 수가 되었을 때 이터레이터 결과 객체의 done 프로퍼티가 true를 반환하는 이터러블 객체이다.

const iterable = {
  [Symbol.iterator]() {
    let i = 1;
    return {
      next() {
        if (i > 9) return { value: undefined, done: true };
        return { value: i++, done: false };
      },
    };
  },
};

for (const v of iterable) {
  console.log(v);
}
// => 1 2 3 4 5 6 7 8 9

위 iterable 객체를 for of 루프를 통해 순회하며 콘솔로 찍어보면 1부터 9까지 잘 출력하는 것을 볼 수 있다.

다음과 같이 이터레이터 객체의 next() 메소드를 통해 1부터 3까지 진행한 후 4부터 for of 루프를 통해 진행시키고 싶은 경우가 있다고 가정해보자.

그러기 위해서는 이터레이터 객체를 for of 루프로 순회할 수 있어야 한다.

방법은 간단하다. 이터레이터 객체이면서 이터러블 객체이면 된다.

즉, 이터레이터 객체의 [Symbol.iterator]() 를 호출하면 재귀적인 형태로 자기 자신을 반환하면 된다. 이러한 이터러블 객체를 Well Formed 이터러블이라고 한다.

const iterable = {
  [Symbol.iterator]() {
    let i = 1;
    return {
      next() {
        if (i > 9) return { done: true };
        return { value: i++, done: false };
      },
      [Symbol.iterator]() {
        return this;
      },
    };
  },
};

위와 같이 만들어 진 이터러블 객체의 이터레이터 객체를 순회하면 원하는 대로 잘 동작하는 것을 알 수 있다.

const iterator = iterable[Symbol.iterator]();

console.log(iterator.next()); // => { value : 1, done: false }
console.log(iterator.next()); // => { value : 2, done: false }
console.log(iterator.next()); // => { value : 3, done: false }

for (const v of iterator) {
  console.log(v);
}
// => 4 5 6 7 8 9

제너레이터

제너레이터 함수

제너레이터는 이터레이터를 조금 더 쉽게 만들 수 있는 ES6의 강력한 새 문법이다.

제너레이터를 만들기 위해서는 반드시 제너레이터 함수를 정의해야 한다.

일반적인 자바스크립트 함수를 만드는 것과 비슷하지만 function 키워드 뒤에 *를 추가한 function* 키워드를 사용한다. 화살표 함수 문법으로 제너레이터 함수를 정의할 수는 없다.

제너레이터 함수를 호출하면 함수 바디를 실행하는 것이 아니라, Well Formed 이터레이터인 제너레이터 객체를 반환한다.

제너레이터 객체의 next() 메서드를 호출하면 제너레이터 함수의 바디가 실행되고 yield 문을 만나면 멈춘다.

즉, next() 메소드를 호출하면 yield 문의 값을 반환한다.

yield는 ES6에 처음 등장했고 return 문과 비슷하다.

하지만 제너레이터 함수에는 return 값을 줄 수도 있다.

제너레이터 함수 내부의 return 값은 done이 true일 때의 value를 정의한다.

아래 예제는 1부터 시작하여 홀수를 출력하는 제너레이터 함수이다.

function* odds(limit) {
  for (let i = 1; i <= limit; i += 2) {
    yield i;
  }
}

for (const num of odds(11)) {
	console.log(num);
}
// => 1 3 5 7 9 11

제너레이터 함수는 이터레이터와 다르게 일반 함수처럼 매개변수를 전달해줄 수 있다.

위 예제에서는 limit 매개변수에 인자를 전달해주어 함수 바디의 for문의 조건을 완성해줄 수 있다.

결과를 보면 1부터 11까지의 홀수를 잘 출력하는 것을 알 수 있다.

yield*

제너레이터를 활용하다 보면, 매개변수를 통해 여러 이터러블을 받아 처리하는 경우가 자주 있다.

아래의 예제는 “abc” 문자열 이터러블과 [1,2,3] 숫자 배열 이터러블을 순서대로 순회하며 전달하는 함수이다.

function* sequence(...iterables) {
  for (const iterable of iterables) {
    for (const item of iterable) {
      yield item;
    }
  }
}

console.log([...sequence("abc", [1, 2, 3])]);
// => [ 'a', 'b', 'c', 1, 2, 3 ]

…(스프레드)연산자를 통해 출력해보면 순서대로 잘 전달된 것을 확인할 수 있다.

이런 경우, 간단히 ES6의 yield* 키워드를 활용할 수 있다.

yield* 키워드는 yield와 비슷하지만 값 하나를 전달하는 것이 아니라 이터러블 객체를 순회하면서 각각의 값을 전달한다.

function* sequence(...iterables) {
  for (const iter of iterables) {
    yield* iter;
  }
}

console.log([...sequence("abc", [1, 2, 3])]);
// => [ 'a', 'b', 'c', 1, 2, 3 ]

앞서 살펴본 예제와 동일한 값이 전달되는 것을 확인할 수 있다.

yield* 키워드를 활용하면 훨씬 간결하고 명확하게 코드를 작성할 수 있다.

이터레이션 프로토콜 활용

빌트인 이터러블

자바스크립트에 내장되어 있는 빌트인 이터러블은 다음과 같다.

  • Array
  • String
  • Map
  • Set
  • TypedArray
  • arguments
  • DOM 컬렉션
    • NodeList
    • HTMLCollection

대표적으로 Array에는 map 함수가 내장되어 있다.

Array에만 사용하는 것이 아니라 이터러블 프로토콜을 따르는 map 함수를 정의하여 이터러블 객체를 모두 순회할 수 있는 다형성이 높은 map 함수를 만들 수 있다.

다형성을 높이는 활용

아래 예제는 콜백함수와 이터러블 객체를 인자로 받아 이터러블 객체를 for of 루프로 순회하며 콜백함수의 결과값을 결과 배열에 넣어 반환한다.

const map = (cb, iterable) => {
  const res = [];
  for (const a of iterable) {
    res.push(cb(a));
  }
  return res;
};

const set = new Set([1, 2, 3, 4, 5]);

map((v) => console.log(v), set);
// => 1 2 3 4 5

콜백함수로 콘솔을 찍는 함수를 전달하였고 Set에서도 map 함수를 실행시킬 수 있는 것을 알 수 있다.

제너레이터는 이터레이션(이터러블/이터레이터) 프로토콜을 따르고 있으므로 해당 프로토콜을 따르고 있는 많은 라이브러리 혹은 내장 함수들과 함께 사용할 수 있다.

 

참고자료

  • 모던 자바스크립트 딥다이브 (저 : 이웅모)
  • 자바스크립트 완벽 가이드(저 : 데이비드 플래너건)
  • 함수형 프로그래밍과 JavaScript ES6+ (인프런, 유인동)

관련글 더보기

댓글 영역