코드스테이츠 부트캠프 프론트엔드 44기
릴레이 블로깅 챌린지 1주차 (월요일)


이터레이터(Iterator)란?

const arr = [1, 2, 3];
const iter = arr[Symbol.iterator]();  // 배열 -> 이터레이터 변환

console.log(iter.next());  // { value: 1, done: false }
console.log(iter.next());  // { value: 2, done: false }
console.log(iter.next());  // { value: 3, done: false }
console.log(iter.next());  // { value: undefined, done: true }
console.log(iter.next());  // { value: undefined, done: true }

이터레이터(Iterator)란 JavaScript의 이터러블한(for~of문으로 돌릴 수 있는) 자료구조들 중 하나이다. Array(배열), Map, Set과는 다른 독특한 특징을 지니고 있다.

필자는 이터레이터를 갑티슈, 그리고 종이컵 디스펜서에 비유하고 싶다.

이터레이터는 데이터를 순서대로 하나씩 빼서 쓰고 싶을 때 사용하는 자료구조이다.

쓰임새

1. 배열을 사용하기 불편한 경우

1.1. 원본 훼손 문제

const arr = [1, 2, 3];

function isOdd() {
  const num = arr.shift();  // 원본 훼손
  return num % 2 === 1;
}

function sumTwoNums() {
  const [x, y] = arr;
  return x + y;
}

console.log(isOdd(), sumTwoNums());  // true, 5
console.log(arr);  // [ 2, 3 ]

만약에 위의 코드가 있다고 해보자.

  • isOdd() : 배열의 1번째 원소를 뽑아서, 그 원소가 홀수인지 체크
  • sumTwoNums() : 그다음 순서인 원소 2개를 뽑아서, 둘을 합한 값을 반환

겉으로는 문제가 없어 보이나, 잘 보면 원본(arr)이 훼손되어 있음을 알 수 있다.

중간에 사용한 arr.shift() 때문에 [1]이 빠져나갔고, 그래서 [1, 2, 3]이었던 원본이 [2, 3]으로 변경되어버린 것이다. 이는 스파게티 코드를 초래하며, 특히나 변경되어선 안 되는 배열의 경우 무결성 원칙을 위반하게 된다.

1.2. 리소스 낭비 문제

원본 훼손 문제를 막기 위해 배열을 복사해서 사용하면 어떨까?

const arr = [1, 2, 3];
const arr2 = [...arr];  // 쓸데없는 리소스 낭비, 가독성 저하

function isOdd() {
  const num = arr2.shift();
  return num % 2 === 1;
}

function sumTwoNums() {
  const [x, y] = arr2;
  return x + y;
}

console.log(isOdd(), sumTwoNums());  // true, 5
console.log(arr);  // [ 1, 2, 3 ]

console.log(arr2);  // [ 2, 3 ]

원본(arr)은 유지됐지만 사본(arr2)이 생김으로써 그만큼 리소스를 잡아먹고 코드가 난잡해진 것을 알 수 있다.

1.3. 가독성 저하 문제

리소스 문제를 막기 위해 인덱스 변수를 따로 만들어준다면?

const arr = [1, 2, 3];
let currentIndex = 0;  // 현재 인덱스를 저장

function isOdd() {
  const num = arr[currentIndex++];
  return num % 2 === 1;
}

function sumTwoNums() {
  const x = arr[currentIndex++];
  const y = arr[currentIndex++];
  return x + y;
}

console.log(isOdd(), sumTwoNums());  // true, 5
console.log(arr);  // [ 1, 2, 3 ]

console.log(currentIndex);  // 3

각 함수마다 currentIndex를 변경해야 하는 코드(++)를 작성해야 하므로 상당히 불편해진다. 게다가 쓸데없는 변수(currentIndex)가 하나 추가되어서 그만큼 코드가 더 난잡해졌고, 모든 함수들이 (함수 바깥에 있는) currentIndex를 변경하기 때문에 Side Effect를 남발하게 된다.

이렇듯 '데이터를 순서대로 빼서 사용'하고 싶을 때 Array(배열)는 적절한 선택이 아니다.

이럴 때 사용하는 게 바로 이터레이터(Iterator)이다.

1.4. 이터레이터로 문제 해결!

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

function isOdd() {
  const num = iter.next().value;
  return num % 2 === 1;
}

function sumTwoNums() {
  const x = iter.next().value;
  const y = iter.next().value;
  return x + y;
}

console.log(isOdd(), sumTwoNums());  // true, 5
console.log(arr);  // [ 1, 2, 3 ]

console.log(iter.next());  // { value: undefined, done: true }

함수 내에서 외부 변수를 변경하지 않으니 Side Effect 문제에서 자유로워졌다. 또한 .next() 메서드 덕분에 데이터가 순서대로 추출되니까, 개발자가 굳이 인덱스를 신경 쓰지 않아도 된다.

게다가 다른 개발자가 해당 코드를 볼 때 '아! 데이터를 순서대로 빼와서 그걸 어떻게 하는 로직이구나!' 하고 쉽게 이해할 수 있다. 이렇듯 이터레이터를 사용하면 의미론적인 리팩토링 효과도 덤으로 기대할 수 있다.


2. 데이터가 무한일 경우

const fibonacci = {
  [Symbol.iterator]() {
    let [pre, cur] = [0, 1];
    return {
      next() {
        [pre, cur] = [cur, pre + cur];
        return { value: cur, done: false };
      }
    };
  }
};
const iter = fibonacci[Symbol.iterator]();

setInterval(() => console.log(iter.next().value), 1000);

이 코드는 '1초에 한 번씩 피보나치 수를 반환 & 출력'한다. 피보나치 수열은 무한히 있기 때문에, 저 코드는 사용자가 수동으로 종료하지 않는 이상 영원히 멈추지 않고 계속 실행된다.

데이터가 무한인 코드는 절대 배열로 만들 수 없다. 왜냐하면 배열은 유한 개의 요소를 담는 자료구조이기 때문이다. 그래서 데이터의 수가 무한대인 자료구조를 만들고 싶다면, 위의 코드처럼 '사용자 정의 이터러블'을 만들어 구현하여야 한다.


3. 최적화

만약에 요소가 1억 개인 자료구조를 만들어서 그걸 다 루프로 순회한다고 해보자.

자료구조로 '배열'을 쓴다면?

const arr = [];

for (let i = 0; i < 100_000_000; i++) {
  arr.push(true);
}

for (const bool of arr) { }

이 코드의 실행 속도는 약 2.3초~2.7초 정도이다.

그중에서 for (const num of arr) { }의 실행 속도는 약 0.6초~1.2초 정도이다.

자료구조로 '이터레이터'를 쓴다면?

const boolIter = {
  [Symbol.iterator]() {
    let i = 0;  // 현재 인덱스
    const max = 100_000_000;  // 마지막 인덱스
    return {
      next() {
        num++;
        return { value: true, done: i > max };
      }
    };
  }
};

for (const bool of boolIter) { }

이 코드의 실행 속도는 약 0.1초이다.

이터레이터의 놀라운 최적화 효과

코드 실행 시간이 무려 20여 분의 1로 단축되었다. 왜 그런 걸까?

  • 【true 1억 개 자료구조 만들기】
    배열의 경우 이걸 만드는 과정부터가 시간이 많이 소요된다. 반면 이더레이터의 경우, 그냥 true를 1억 번 뽑아낼 수 있는 '생성기'를 하나 만들면 끝난다. 때문에 이더레이터는 사실상 0초 만에 자료구조가 완성된다.

  • 【true 1억 개 자료구조 순회하기】
    배열을 for문으로 돌릴 경우, 1억 개 용량의 매우 긴 배열을 인덱스 순서대로 추출하는 작업을 1억 번 반복하여야 한다. 당연하겠지만 매우 비효율적이다. 반면 이더레이터의 경우, 그냥 다음 값 뽑아내는 것만 하면 되기 때문에 내부 로직이 상당히 간단하고 가볍다.

이러한 장점들 덕분에 이터레이터는 코드 최적화에서 곧잘 사용된다.



<주의 사항>

이 게시물은 코드스테이츠의 블로깅 과제로 제작되었습니다.
때문에 설명이 온전치 못하거나 글의 완성도가 낮을 수 있습니다.

profile
Self-improvement Guarantees Future.

2개의 댓글

comment-user-thumbnail
2023년 4월 17일

곽티슈 비유가 완전 찰떡이네요! 이터레이터가 최적화 측면에서도 효과가 있을거라고는 생각 못했는데 많이 배워갑니다

1개의 답글