이터레이터와 이터러블
JavaScript 개발자라면 반드시 알아야 할 중요한 개념 중 하나가 바로 이터레이터(Iterator)와 이터러블(Iterable)이다. 이 두 개념은 데이터 컬렉션을 효율적으로 순회하고 처리하는 데 핵심적인 역할을 한다. 이 글에서는 이터레이터와 이터러블의 개념, 작동 방식, 그리고 실제 활용 사례를 깊이 있게 살펴볼 것이다.
이터레이터 패턴의 역사와 발전
이터레이터 패턴은 객체 지향 프로그래밍에서 오래된 디자인 패턴 중 하나다. 이 패턴의 주요 목적은 컬렉션의 내부 구조를 노출하지 않고도 모든 요소에 순차적으로 접근할 수 있게 하는 것이다.
JavaScript에서 이터레이터와 이터러블의 개념은 ES6(ECMAScript 2015)에서 공식적으로 도입되었다. 이전 버전의 JavaScript에서도 유사한 패턴을 구현할 수 있었지만, ES6에서 표준화됨으로써 언어 차원에서의 지원이 가능해졌다.
이터레이터와 비동기 프로그래밍
최근 JavaScript의 발전과 함께, 비동기 이터레이터와 비동기 이터러블의 개념도 등장했다. 이는 비동기 작업을 순차적으로 처리해야 하는 상황에서 매우 유용하다.
async function* asyncGenerator() {
yield await Promise.resolve(1);
yield await Promise.resolve(2);
yield await Promise.resolve(3);
}
(async () => {
for await (let num of asyncGenerator()) {
console.log(num);
}
})();
이 예시에서는 비동기 제너레이터를 사용하여 비동기적으로 값을 생성하고, for await...of 루프를 통해 이를 순회한다.
이터레이터(Iterator)란?
이터레이터는 컬렉션의 요소를 순회하는 방법을 정의하는 객체다. 주요 특징은 다음과 같다.
1. next() 메서드
이터레이터의 핵심 메서드로, 컬렉션의 다음 요소를 반환한다.
2. 반환 객체
next() 메서드는 {value: any, done: boolean} 형태의 객체를 반환한다.
- value: 현재 요소의 값
- done: 순회가 완료되었는지를 나타내는 불리언 값
이터레이터의 기본 구조는 다음과 같다.
const iterator = {
next: function() {
// 다음 요소 반환 로직
return { value: nextValue, done: isComplete };
}
};
이터러블(Iterable)이란?
이터러블은 순회 가능한 객체를 의미한다. 이터러블의 주요 특징은 다음과 같다.
Symbol.iterator 메서드
이터레이터를 반환하는 메서드를 가지고 있다.
for...of 루프 사용 가능
이터러블 객체는 for...of 루프를 통해 쉽게 순회할 수 있다.
이터러블의 기본 구조는 다음과 같다.
const iterable = {
[Symbol.iterator]: function() {
// 이터레이터 객체 반환
return {
next: function() {
// 다음 요소 반환 로직
}
};
}
};
이터레이터와 이터러블의 관계
이터레이터와 이터러블은 밀접한 관계를 가지고 있다. 이터러블은 이터레이터를 생성하는 객체이며, 이터레이터는 실제로 요소를 순회하는 메커니즘을 제공한다. 이 관계를 통해 JavaScript에서는 다양한 데이터 구조를 일관된 방식으로 순회할 수 있게 된다.
내장 이터러블 객체
JavaScript에는 기본적으로 이터러블한 여러 객체들이 있다.
- 배열(Array)
- 문자열(String)
- Map
- Set
- TypedArray
- arguments 객체
- NodeList
이러한 객체들은 모두 Symbol.iterator 메서드를 가지고 있어, for...of 루프나 스프레드 연산자 등을 통해 쉽게 순회할 수 있다.
커스텀 이터러블 만들기
자신만의 이터러블 객체를 만드는 것도 가능하다. 다음은 간단한 예시다.
const customIterable = {
data: [1, 2, 3, 4, 5],
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.data.length) {
return { value: this.data[index++], done: false };
} else {
return { done: true };
}
}
};
}
};
for (let item of customIterable) {
console.log(item); // 1, 2, 3, 4, 5 출력
}
이 예시에서 customIterable 객체는 Symbol.iterator 메서드를 구현하여 이터러블이 되었다. 이 메서드는 next 메서드를 가진 이터레이터 객체를 반환한다.
제너레이터(Generator)
제너레이터는 이터레이터를 생성하는 특별한 종류의 함수다. function* 문법을 사용하여 정의되며, yield 키워드를 통해 값을 생성한다. 제너레이터는 이터레이터와 이터러블의 개념을 더욱 간단하게 구현할 수 있게 해준다.
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = numberGenerator();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
제너레이터 함수는 호출될 때마다 이터레이터를 반환하며, 이 이터레이터는 함수 내부의 yield 문을 만날 때마다 실행을 일시 중지하고 값을 반환한다.
이터레이터와 이터러블의 활용
이터레이터와 이터러블은 다양한 상황에서 유용하게 활용될 수 있다.
1. 데이터 스트리밍
대용량 데이터를 처리할 때, 메모리 효율성을 높일 수 있다.
2. 지연 평가(Lazy Evaluation)
필요한 시점에만 데이터를 생성하여 성능을 개선할 수 있다.
3. 무한 시퀀스
이론적으로 무한한 데이터 시퀀스를 표현할 수 있다.
4. 커스텀 데이터 구조
복잡한 데이터 구조를 쉽게 순회할 수 있게 만들 수 있다.
실제 사용 예시
1. 피보나치 수열 생성기
피보나치 수열을 생성하는 이터러블 객체를 만들어보자.
const fibonacci = {
[Symbol.iterator]() {
let prev = 0, curr = 1;
return {
next() {
[prev, curr] = [curr, prev + curr];
return { value: prev, done: false };
}
};
}
};
for (let num of fibonacci) {
if (num > 1000) break;
console.log(num);
}
이 예시에서는 피보나치 수열을 무한히 생성하는 이터러블 객체를 만들었다. for...of 루프를 사용하여 1000 이하의 피보나치 수를 출력한다.
2. 페이지네이션 구현
API에서 데이터를 페이지 단위로 가져오는 상황을 가정해보자.
function* paginator(apiFunction, pageSize = 10) {
let currentPage = 1;
while (true) {
const data = yield apiFunction(currentPage, pageSize);
if (data.length === 0) break;
currentPage++;
}
}
// 가상의 API 함수
function fetchData(page, pageSize) {
// API 호출 로직
return [/* 데이터 */];
}
const pageIterator = paginator(fetchData);
let result = pageIterator.next();
while (!result.done) {
console.log(result.value); // 페이지 데이터 처리
result = pageIterator.next(result.value);
}
이 예시에서는 제너레이터를 사용하여 페이지네이션을 구현했다. 각 yield는 새로운 페이지의 데이터를 반환하며, 데이터가 더 이상 없을 때 루프가 종료된다.
이터레이터와 이터러블의 장단점
장점
1. 메모리 효율성
전체 데이터를 한 번에 메모리에 로드하지 않고, 필요한 만큼만 처리할 수 있다.
2. 유연성
다양한 데이터 소스에 대해 일관된 인터페이스를 제공한다.
3. 지연 평가
필요한 시점에 데이터를 생성하므로 불필요한 연산을 줄일 수 있다.
4. 무한 시퀀스
이론적으로 무한한 데이터 시퀀스를 다룰 수 있다.
단점
1. 복잡성
기본적인 루프에 비해 개념 이해와 구현이 복잡할 수 있다.
2. 디버깅의 어려움
지연 평가로 인해 디버깅이 더 어려울 수 있다.
3. 성능 오버헤드
간단한 경우에는 일반 루프보다 성능이 떨어질 수 있다.
'프로그래밍 > 파이썬' 카테고리의 다른 글
파이썬 - 데코레이터(Decorator)의 이해와 구현 (0) | 2024.12.01 |
---|---|
파이썬 - 람다 함수와 함수형 프로그래밍 기초 (0) | 2024.12.01 |
파이썬 - 정규표현식 (0) | 2024.11.30 |
파이썬 문자열 포매팅: f-string, str.format(), % 연산자 비교 (0) | 2024.11.30 |
파이썬 - 불변(Immutable)과 가변(Mutable) 객체 (0) | 2024.11.29 |