Iterator Pattern 에 대하여

이터레이터 패턴은 이터레이터 객체를 활용하여 컬렉션의 요소들을
순차적으로 접근할 수 있도록 설계된 디자인 패턴입니다.
이를 통해 배열, 리스트, 해시맵 등 다양한 자료구조와 상관없이
일관된 인터페이스를 사용하여 요소를 순회할 수 있게 됩니다.
Iterator Pattern
const m = new Map();
m.set('name', 'do-not-do-that');
m.set('age', 65);
m.set('city', 'seoul');
const s = new Set();
s.add(10);
s.add(20);
s.add(30);
for (let item of m) console.log(item);
for (let item of s) console.log(item);
/**
['name', 'do-not-do-that']
['age', 65]
['city', 'seoul']
10
20
30
*/
Map 과 Set 은 서로 다른 자료구조이지만, 이터러블(Iterable) 프로토콜을 따르기 때문에 for ... of 문을 사용하여 동일한 방식으로 순회할 수 있습니다.
Map 의 경우 [key, value] 형태의 배열을 반환하며, Set은 단일 값을 반환하는 구조를 가집니다.
이처럼 다양한 컬렉션이 일관된 이터레이터 인터페이스를 구현하고 있어,
내부 구조와 상관없이 동일한 방식으로 순회할 수 있다는 것이 이터레이터 패턴의 핵심 장점입니다.
용어 정리 ✍🏻
이터레이터 VS 이터러블
이터레이터
란, {value: 값, done: true / false} 형태의 이터레이터 객체를 리턴하는 next() 메서드를 가진 객체입니다.이터러블
이란, 이터레이터를 리턴하는 [Symbol.iterator]() 메서드를 가진 객체입니다.
JavaScript 에서의 이터레이터 패턴 구현
JS에서는 이터레이터 패턴을 두 가지 방식으로 구현합니다.
- 커스텀 이터레이터 구현
- 제너레이터 함수
하나씩 직접 코드로 살펴봅시다.
기본 이터레이터 구현
JavaScript에서 이터레이터는 next() 메서드를 구현하는 객체입니다.
이 메서드는 순회할 값과 done 상태를 반환하는 객체를 리턴해야 합니다.
class CustomIterator {
constructor(data) {
this.index = 0;
this.data = data;
}
next() {
if(this.index < this.data.length) {
return { value: this.data[this.index++], done: false };
}else {
return { value: undefined, done: true }
}
}
}
// 실제 사용
const iterable = [10, 20, 30, 40];
const iterator = new CustomIterator(iterable);
console.log(iterater.next()); // { value: 10, done: false }
console.log(iterater.next()); // { value: 20, done: false }
console.log(iterater.next()); // { value: 30, done: false }
console.log(iterater.next()); // { value: 40, done: false }
console.log(iterater.next()); // { value: undefined, done: true }
- CustomIterator 클래스는 배열을 순회할 수 있는 커스텀 이터레이터 입니다.
- next() 메서드를 호출하면 다음 요소를 반환하며, 더 순회할 요소가 없으면
done:true
를 반환합니다. - for...of 문에서 사용할 수는 없으며, next() 를 직접 호출해야합니다.
이터러블 프로토콜을 구현한 객체 만들기
위의 예제는 이터레이터 프로토콜만 구현했기 때문에, for...of
문을 사용할 수 없습니다.
만약 이터러블 프로토콜을 구현하면 이터레이터 객체를 for...of
문과 함께 사용할 수 있습니다.
const customIterable = {
data = ["apple", "banana", "cherry"],
index = 0,
[Symbol.iterator]() {
return {
data: this.data,
index: this.index,
next() {
if (this.index < this.data.length) {
return { value: this.data[this.index++], done: false };
}else {
return { value: undefined, done: true };
}
}
}
}
}
for (const fruit of customIterable) {
console.log(fruit);
}
// apple
// banana
// cherry
- [Symbol.iterator]() 메서드를 추가해 이터러블 프로토콜을 구현했습니다.
- 이터러블 프로토콜이 구현되었기 때문에,
for...of
문에서 사용할 수 있는 것을 확인할 수 있습니다. - next() 를 직접 호출하는 방식보다 좀 더 가독성이 좋아졌습니다.
좀 더 재밌는걸 해보겠습니다.
const arr = ['home', 'sweet', 'candy'];
for (const favorite of arr) {
console.log(favorite);
}
// home
// sweet
// candy
위 코드를 실행시키면, 자연스럽게 arr 배열 내의 모든 요소들을 순회하며 출력합니다.
const arr = ['home', 'sweet', 'candy'];
arr[Symbol.iterator] = null;
for (const favorite of arr) {
console.log(favorite);
}
이제 위와 같이 Symbol.iterator 를 의도적으로 null 로 변환시키면,

이터러블 객체가 아니기 때문에, 더이상 for...of 로 순회할 수 없음을 볼 수 있습니다.
제너레이터를 활용한 이터레이터
제너레이터 함수는 이터레이터 패턴을 구현할 때 훨씬 간결한 코드를 만들 수 있습니다.
제너레이터는 yield
키워드를 사용해 값을 하나씩 반환하는 함수입니다.
function* numberGenerator(){
yield 1;
yield 2;
yield 3;
yield 4;
}
const generator = numberGenerater();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: 4, done: false }
console.log(generator.next()); // { value: undefined, done: true }
function*
문법을 사용하면 제너레이터 함수를 정의할 수 있습니다.yield
키워드를 사용해 값을 하나씩 반환합니다.- next() 를 호출하면 다음 yield 지점까지 실행된 후 중단됩니다.
마무리
이터레이터 패턴은 요소를 순차적으로 접근할 수 있도록 해주는 디자인 패턴입니다.
이를 통해 배열, 객체 등 다양한 컬렉션을 일관된 방식으로 순회할 수 있습니다.
비록 저는 실무에서 이터레이터 패턴을 직접 구현해서 사용하는 경우를 보지는 못했으나,
JavaScript의 Map, Set, for...of 등이 기본적으로 이터레이터 패턴을 따르고 있다는 점을 생각하면 우리가 이미 자연스럽게 사용하고 있다고 볼 수 있겠습니다.
이 글이 도움이 되었길 바라며, 읽어주셔서 감사합니다!