코드스테이츠 부트캠프 프론트엔드 44기
릴레이 블로깅 챌린지 1주차 (월요일)
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과는 다른 독특한 특징을 지니고 있다.
필자는 이터레이터를 갑티슈, 그리고 종이컵 디스펜서에 비유하고 싶다.
이터레이터는 데이터를 순서대로 하나씩 빼서 쓰고 싶을 때 사용하는 자료구조이다.
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 ]
만약에 위의 코드가 있다고 해보자.
겉으로는 문제가 없어 보이나, 잘 보면 원본(arr)이 훼손되어 있음을 알 수 있다.
중간에 사용한 arr.shift() 때문에 [1]이 빠져나갔고, 그래서 [1, 2, 3]이었던 원본이 [2, 3]으로 변경되어버린 것이다. 이는 스파게티 코드를 초래하며, 특히나 변경되어선 안 되는 배열의 경우 무결성 원칙을 위반하게 된다.
원본 훼손 문제를 막기 위해 배열을 복사해서 사용하면 어떨까?
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)이 생김으로써 그만큼 리소스를 잡아먹고 코드가 난잡해진 것을 알 수 있다.
리소스 문제를 막기 위해 인덱스 변수를 따로 만들어준다면?
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)이다.
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() 메서드 덕분에 데이터가 순서대로 추출되니까, 개발자가 굳이 인덱스를 신경 쓰지 않아도 된다.
게다가 다른 개발자가 해당 코드를 볼 때 '아! 데이터를 순서대로 빼와서 그걸 어떻게 하는 로직이구나!' 하고 쉽게 이해할 수 있다. 이렇듯 이터레이터를 사용하면 의미론적인 리팩토링 효과도 덤으로 기대할 수 있다.
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초에 한 번씩 피보나치 수를 반환 & 출력'한다. 피보나치 수열은 무한히 있기 때문에, 저 코드는 사용자가 수동으로 종료하지 않는 이상 영원히 멈추지 않고 계속 실행된다.
데이터가 무한인 코드는 절대 배열로 만들 수 없다. 왜냐하면 배열은 유한 개의 요소를 담는 자료구조이기 때문이다. 그래서 데이터의 수가 무한대인 자료구조를 만들고 싶다면, 위의 코드처럼 '사용자 정의 이터러블'을 만들어 구현하여야 한다.
만약에 요소가 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억 번 반복하여야 한다. 당연하겠지만 매우 비효율적이다. 반면 이더레이터의 경우, 그냥 다음 값 뽑아내는 것만 하면 되기 때문에 내부 로직이 상당히 간단하고 가볍다.
이러한 장점들 덕분에 이터레이터는 코드 최적화에서 곧잘 사용된다.
<주의 사항>
이 게시물은 코드스테이츠의 블로깅 과제로 제작되었습니다.
때문에 설명이 온전치 못하거나 글의 완성도가 낮을 수 있습니다.
곽티슈 비유가 완전 찰떡이네요! 이터레이터가 최적화 측면에서도 효과가 있을거라고는 생각 못했는데 많이 배워갑니다