- 이 글은 A Simple Guide to ES6 Iterators in JavaScript with Examples를 번역한 글입니다.
- 아직 입문자이다보니 오역을 한 경우가 있을 수 있습니다. 양해 부탁드립니다.
- 매끄러운 문맥을 위하여 의역을 한 경우가 있습니다. 원문의 뜻을 최대한 해치지 않도록 노력했으니 안심하셔도 됩니다.
- 영어 단어가 자연스러운 경우 원문 그대로의 영단어를 적었습니다.
- 저의 보충 설명은 인용문에 달았습니다.
이 글에서는 반복자(Iterator)를 분석하고 있습니다. 자바스크립트의 컬렉션을 순회하는 새로운 방식으로 반복자를 사용할 수 있습니다. 반복자는 ES6에서 소개되었고, 활용도가 풍부하고 다양한 경우에 사용되고 있어 높은 인기를 끌게 되었습니다.
반복자가 무엇인지, 그리고 어디서 사용되는지 그 예시를 개념적으로 이해하고, 자바스크립트로 구현된 모습을 살펴보도록 하겠습니다.
이 글에서
Iterator
는 반복자로 번역합니다. Iterable은 내부에 반복자를 가져서 각 요소를 순회할 수 있는 객체를 가리키는 형용사로, 반복 가능한 또는 반복 가능한 객체 로 번역합니다.
아래와 같은 배열을 가정해봅시다.
const myFavouriteAuthors = [
'Neal Stephenson',
'Arthur Clarke',
'Isaac Asimov',
'Robert Heinlein'
];
배열의 각 요소를 모두 가져와서 화면에 출력하거나, 각각을 조작하거나, 어떤 작업을 수행해야 한다는 요청을 받았을 때에 어떻게 하시겠습니까? "그건 쉽죠. 그냥 for
, while
, for-of
, 아니면 그 밖의 반복문을 써서 하나씩 돌리면 되겠네요." 구현 예시는 아래와 같은 모습을 띨 것입니다.
/* 배열을 다루는 다양한 반복 기법 */
// for loop
for (let index = 0; index < myFavouriteAuthors.length; index++) {
console.log(myFavouriteAuthors[index]);
}
// while loop
let index = 0;
while (index < myFavouriteAuthors.length) {
console.log(myFavouriteAuthors[index]);
index++;
}
// for-of loop
for (const value of myFavouriteAuthors) {
console.log(value);
}
자, 이제 위의 배열이 아니라 작가 목록을 보유하는 별도의 자료 구조가 있다고 가정해봅시다. 아래와 같이 말이죠.
/* 별도의 자료 구조 */
const myFavouriteAuthors = {
allAuthors: {
fiction: [
'Agatha Christie',
'J. K. Rowling',
'Dr. Seuss'
],
scienceFiction: [
'Neal Stephenson',
'Arthur Clarke',
'Isaac Asimov',
'Robert Heinlein'
],
fantasy: [
'J. R. R. Tolkien',
'J. K. Rowling',
'Terry Pratchett'
],
},
}
여기서 myFavouriteAuthors
는 또다른 객체 allAuthors
를 포함하는 객체입니다. allAuthors
는 각각 fiction
, scienceFiction
, fantasy
가 키인 세 개의 배열을 포함하고 있습니다. 자, 여기서 만약 myFavouriteAuthors
를 돌면서 모든 작가를 가져오라는 요청을 받는다면, 어떤 접근법을 사용하겠습니까? 그냥 단순히 여러 반복 기법을 조합해서 사용할 수도 있겠죠.
하지만, 만약 이렇게 한다면 어떨까요?
for (let author of myFavouriteAuthors) {
console.log(author)
}
// TypeError: {} is not iterable
해당 객체는 반복 가능하지 않다는 TypeError
를 얻을 겁니다. 반복 가능한 객체가 무엇인지. 어떻게 하면 객체를 반복 가능하게 만들 수 있는지 알아보도록 합시다. 이 글을 다 읽고 나면, 별도로 정의한 객체, 이 글의 경우 myFavouriteAuthors
에 대하여 for-of
반복문을 어떻게 사용하는지 알 수 있을 겁니다.
바로 직전의 절에서 문제를 확인했습니다. 별도로 정의된 객체에서 모든 작가들을 가져오는 것은 쉽지 않았습니다. 내부의 데이터를 순서대로 노출시킬 수 있는 그런 메서드가 필요합니다.
myFavouriteAuthors
에 getAllAuthors
라는 이름의 메서드를 추가하고, 이 메서드가 모든 작가들을 반환하도록 만들어봅시다. 이렇게 말이죠.
/* getAllAuthors의 구현 */
const myFavouriteAuthors = {
allAuthors: {
...
},
getAllAuthors() {
const authors = [];
for (const author of this.allAuthors.fiction) {
authors.push(author);
}
for (const author of this.allAuthors.scienceFiction) {
authors.push(author);
}
for (const author of this.allAuthors.fantasy) {
authors.push(author);
}
return authors;
}
}
아주 간단한 접근입니다. 이렇게 하면 작가를 모두 가져온다는 현재의 임무를 잘 완수할 수 있습니다. 하지만, 이 구현 방식으로는 몇가지 문제가 발생합니다.
getAllAuthors
라는 이름은 범용적이지 않습니다. 만약 누군가 자신만의 myFavouriteAuthors
를 만든다면, 이름을 retrieveAllAuthors
라고 지어버릴 수도 있습니다.getAllAuthors
가 이 경우에 해당하겠군요.getAllAuthors
는 모든 작가의 이름을 문장의 배열로써 반환합니다. 만약 어떤 개발자가 다음과 같이 객체의 배열로 반환하면 어떻게 할까요?[ {name: 'Agatha Christie'}, {name: 'J. K. Rowling'}, ... ]
개발자는 모든 데이터를 반환하는 메서드의 정확한 이름, 그리고 반환 타입을 확실히 알아야만 할 것입니다.
만약, 이 메서드의 이름과 반환 타입이 고정되고, 바꿀 수 없다는 규칙을 만드는 것은 어떨까요?
이 메서드를 iteratorMethod 라고 부르기로 합시다.
ECMA는 이와 비슷한 느낌의 과정을 거쳐서 사용자 정의 객체를 순회하는 과정을 표준화했습니다. 하지만 iteratorMethod
라는 이름 대신에, ECMA는 Symbol.iterator
라는 이름을 채택했습니다. Symbol은 고유하면서 다른 속성 이름과 충돌할 수 없는 이름을 제공합니다. 또한, Symbol.iterator
는 iterator
(반복자)라는 객체를 반환합니다. 이 반복자는 next
메서드를 가지며, 이 메서드는 value
와 done
이라는 키를 갖는 객체를 반환합니다.
value
는 현재값을 가집니다. value
로는 어떤 자료형도 올 수 있습니다. done
은 불리언입니다. done
은 객체의 모든 값이 사용되었는지를 표시해줍니다.
아래의 도형을 보면 반복 가능한 객체, 반복자, next
메서드 간의 관계를 머릿속에 그리는 데에 도움이 될 겁니다. 이 관계를 반복 프로토콜(Iteration Protocol)이라고 부릅니다.
Axel Rauschmayer 박사의 저서 Exploring JS에 따르면,
Symbol.iterator
를 키로 가지는 메서드를 구현하면 가능합니다. 이 메서드는 반복자를 만드는 팩토리 메서드입니다. 즉, 이 메서드를 통하여 반복자가 생성됩니다.위에서
for-of
구문을 사용했을 때 발생한 오류 메세지에서 확인할 수 있듯 자바스크립트의 객체는 기본적으로 반복 가능하지 않습니다. 따라서 이번 절에서 수행하는 것과 같은 방식으로 반복 프로토콜을 구현해야 합니다.
이전 절에서 배웠듯이, Symbol.iterator
라는 메서드를 구현해야 합니다. 이 키를 설정하기 위하여 속성 계산 문법을 사용하겠습니다. 짧은 예시는 다음과 같습니다.
/* 반복 가능한 객체의 에시 */
const iterable = {
[Symbol.iterator]() {
let step = 0;
const iterator = { // 04
next() {
step++;
if (step === 1) {
return { value: 'This', done: false };
} else if (step === 2) {
return { value: 'is', done: false};
} else if (step === 3) {
return { value: 'iterable', done: false };
}
return { value: undefined, done: true };
}
};
return iterator;
}
};
var iterator = iterable[Symbol.iterator](); // 25
iterator.next() // { value: 'This', done: false } // 27
iterator.next() // { value: 'is', done: false }
iterator.next() // { value: 'iterable', done: false }
iterator.next() // { value: undefined, done: true }
코드의 4번째 줄을 보면, 반복자가 만들어집니다. 이 객체에는 next
메서드가 정의되어있습니다. next
메서드는 step
변수에 따라 값을 반환합니다. 25번째 줄에서 반복자 iterator
가 반환됩니다. 27번째 줄에서는 next
를 호출합니다. done
의 값이 true
가 될 때까지 next
를 계속 호출할 수 있습니다.
반복 프로토콜은 말 그대로 하나의 규약입니다. 반드시 위의 코드와 같이
[Symbol.iterator]
라는 이름의 메서드를 만들지 않더라도 반복자의 기능을 구현하는 데에는 아무런 문제가 없습니다. 다만 반복자의 특성을 활용한 자바스크립트의 내장 기능들을 사용하기 위하여 반복 프로토콜에 따라 속성을 정의하는 것일 뿐입니다.
이것이 바로 for-of
반복문 내에서 벌어지는 일입니다. for-of
반복문은 반복 가능한 객체를 받아서 그것의 반복자를 만들어냅니다. 그리고 done
아 true
가 될 때까지 next()
를 반복적으로 호출합니다.
자바스크립트에서는 반복 가능한 객체가 다양하게 존재합니다. 바로 보이지는 않겠지만 자세히 들여다보면, 반복 가능한 객체들이 보이기 시작할 겁니다.
아래의 것들이 전부 반복 가능한 객체들입니다.
arguments
: 함수 내에 존재하는 유사 배열 객체반복 가능한 객체를 사용하는 JS의 문법들은 다음과 같습니다.
for-of
반복문: 반복 가능한 객체를 제공받지 못하면 TypeError
를 던집니다.for (const value of iterable) { ... }
const array = ['a', 'b', 'c', 'd', 'e'];
const [first, , third, , last] = array;
위의 코드는 아래의 것과 동등합니다.
const array = ['a', 'b', 'c', 'd', 'e'];
const iterator = array[Symbol.iterator]();
const first = iterator.next().value
iterator.next().value // 이 값은 넘어갔으므로, 할당이 이루어지지 않습니다
const third = iterator.next().value
iterator.next().value // 이 값은 넘어갔으므로, 할당이 이루어지지 않습니다
const last = iterator.next().value
const array = ['a', 'b', 'c', 'd', 'e'];
const newArray = [1, ...array, 2, 3];
위의 코드는 아래의 것과 동등합니다.
const array = ['a', 'b', 'c', 'd', 'e'];
const iterator = array[Symbol.iterator]();
const newArray = [1];
for (let nextValue = iterator.next(); nextValue.done !== true; nextValue = iterator.next()) {
newArray.push(nextValue.value);
}
newArray.push(2)
newArray.push(3)
Promise.all
과 Promise.race
는 프라미스들로부터 반복 가능한 객체를 받습니다.Map의 생성자는 [키, 값]의 쌍에 대한 반복 가능한 객체를 Map으로, Set의 생성자는 각 요소들에 대한 반복 가능한 객체를 Set으로 변환합니다.
const map = new Map([[1, 'one'], [2, 'two']]);
map.get(1)
// one
const set = new Set(['a', 'b', 'c]);
set.has('c');
// true
아래의 코드는 myFavouriteAuthors
를 반복 가능한 객체로 만들어줍니다.
/* Sample implementation of iterable */
const myFavouriteAuthors = {
allAuthors: {
fiction: [
'Agatha Christie',
'J. K. Rowling',
'Dr. Seuss'
],
scienceFiction: [
'Neal Stephenson',
'Arthur Clarke',
'Isaac Asimov',
'Robert Heinlein'
],
fantasy: [
'J. R. R. Tolkien',
'J. K. Rowling',
'Terry Pratchett'
],
},
[Symbol.iterator]() {
// 모든 작가들을 배열에 가져온다
const genres = Object.values(this.allAuthors);
// 현재 장르와 작가에 대한 인덱스를 저장한다
let currentAuthorIndex = 0;
let currentGenreIndex = 0;
return {
// next()의 구현
next() {
// 현재 genre 인덱스에 대한 작가들
const authors = genres[currentGenreIndex];
// doNotHaveMoreAuthors는 authors 배열이 비었을 때에 true이다.
// 즉, 모든 항목들이 소비가 완료되었을 때이다.
const doNotHaveMoreAuthors = !(currentAuthorIndex < authors.length);
if (doNotHaveMoreAuthors) {
// 그 때가 되면, genre 인덱스를 다음 장르로 이동시킨다.
currentGenreIndex++;
// 그리고 author 인덱스를 0으로 재설정하고 새로운 작가 셋을 가져온다
currentAuthorIndex = 0;
}
// 모든 장르가 끝이 나면, 반복기에게 더 이상 값이 없다는 사실을 알려야 한다.
const doNotHaveMoreAuthors = !(currentGenreIndex < genres.length)
if (doNotHaveMoreAuthors) {
// 따라서, done을 true로서 반환한다
return {
value: undefined,
done: true
};
}
// 모든 것이 제대로 작동하였다면, 현재 장르에 대한 작가 목록을 반환하고
// currentAuthorIndex를 증가시켜서 다음 번에 새로운 작가가 반환될 수 있도록 만든다.
return {
value: genres[currentGenreIndex][currentAuthorIndex++],
done: false
}
}
};
}
};
for (const author of myFavouriteAuthors) {
console.log(author);
}
console.log(...myFavouriteAuthors)
이 글을 통하여 반복자의 동작 원리를 쉽게 이해할 수 있을 것입니다. 이 로직은 따라오기 조금 어려울 수 있습니다. 그래서 코드 내에 주석을 달아서 부연 설명을 했습니다. 하지만 이해와 내면화의 가장 좋은 방법은 브라우저나 Node 상에서 코드를 직접 실행해보며 가지고 노는 것입니다.
좋은글 감사드립니다. 이터레이터를 이해하는데 많은 도움이 되었습니다 :)