예시를 통하여 쉽게 알아보는 ES6 Iterator

  • 이 글은 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 반복문을 어떻게 사용하는지 알 수 있을 겁니다.

반복 가능한 객체와 반복자

바로 직전의 절에서 문제를 확인했습니다. 별도로 정의된 객체에서 모든 작가들을 가져오는 것은 쉽지 않았습니다. 내부의 데이터를 순서대로 노출시킬 수 있는 그런 메서드가 필요합니다.

myFavouriteAuthorsgetAllAuthors라는 이름의 메서드를 추가하고, 이 메서드가 모든 작가들을 반환하도록 만들어봅시다. 이렇게 말이죠.

/* 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.iteratoriterator(반복자)라는 객체를 반환합니다. 이 반복자는 next 메서드를 가지며, 이 메서드는 valuedone이라는 키를 갖는 객체를 반환합니다.

value는 현재값을 가집니다. value로는 어떤 자료형도 올 수 있습니다. done은 불리언입니다. done은 객체의 모든 값이 사용되었는지를 표시해줍니다.

아래의 도형을 보면 반복 가능한 객체, 반복자, next 메서드 간의 관계를 머릿속에 그리는 데에 도움이 될 겁니다. 이 관계를 반복 프로토콜(Iteration Protocol)이라고 부릅니다.

1_n_qjdraSfty1wj55qt3OxQ.png

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 반복문은 반복 가능한 객체를 받아서 그것의 반복자를 만들어냅니다. 그리고 donetrue가 될 때까지 next()를 반복적으로 호출합니다.

자바스크립트에서의 반복 가능한 객체

자바스크립트에서는 반복 가능한 객체가 다양하게 존재합니다. 바로 보이지는 않겠지만 자세히 들여다보면, 반복 가능한 객체들이 보이기 시작할 겁니다.

아래의 것들이 전부 반복 가능한 객체들입니다.

  • ArrayTypedArray
  • String: 각 문자 또는 유니코드 코드 포인트를 순회
  • Map: 각각의 키-값 쌍을 순회
  • Set: 각각의 요소를 순회
  • 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
  • Spread 연산자 (...)
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.allPromise.race는 프라미스들로부터 반복 가능한 객체를 받습니다.
  • Map과 Set

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을 반복 가능한 객체로 만들기

아래의 코드는 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 상에서 코드를 직접 실행해보며 가지고 노는 것입니다.