들어가기 전에

자바스크립트 개발자라면 알아야 할 33가지 개념 #24-1 자바스크립트 : 예제와 함께 자바스크립트 ES6 iterator 이해하기

Iterator

자바스크립트의 Iterator를 분석해봅시다. Iterator는 자바스크립트의 collection을 반복하는 새로운 방법입니다. ES6에서 소개된 개념이고 매우 유용하고 많은 곳에서 사용되고 있기 때문에 인기가 많습니다.

우리는 개념적으로 Iterator가 어떤 것인지 이해하고 예제와 함께 어디에 사용해야 하는지 배워볼 것입니다. 그리고 자바스크립트 내부에서 Iterator가 구현된 곳도 살펴볼 것입니다.

소개

다음과 같은 배열이 있다고 가정해봅시다.

const myFavouriteAuthors = [
  'Neal Stephenson',
  'Arthur Clarke',
  'Isaac Asimov', 
  'Robert Heinlein'
];

우리가 주로 하고 싶은 작업들은 배열의 값들을 꺼내서 화면에 출력하거나, 배열 값들을 수정하거나, 또는 배열 값들을 이용하여 무언가를 수행하고 싶어하겠죠. 만일 제가 여러분들에게 어떻게 할 것이냐고 물어본다면, 아마 for, while, for-of를 이용하여 반복문을 돌려 작업을 수행할 것이라고 말하는 사람들이 있을 겁니다. 예제의 구현은 다음과 같을 것입니다.

loop1.png

배열에서의 다양한 반복 테크닉

이제, 여러분이 이전의 배열과 같은 형태의 데이터가 아닌, 아래와 같이 사용자 설정의 데이터 구조를 가진 데이터를 갖고 있다고 생각해보면...

custom_data.png

Custom Data Structure

이제, myFavouriteAuthors는 또 다른 객체인 allAuthors를 가진 객체입니다. allAuthorsfiction, scienceFiction, fantasy 라는 키를 가진 3개의 배열로 이루어져 있습니다. 지금 여러분에게 모든 작가 정보를 가져오기 위해서 myFavouriteAuthors에 대한 루프를 돌아보라고 하면, 여러분의 접근법은 어떻게 될까요? 모든 데이터를 가져오기 위해서 반복을 조합하려 할 것입니다.

만일, 이전처럼 하려고 한다면...

for (let author of myFavouriteAuthors) {
  console.log(author);
}

// 타입 에러: {} 는 반복할 수 없습니다.

여러분은 객체(Object)를 반복할 수 없다(not iterable)TypeError를 만나게 될 것입니다. 반복할 수 있다는 것(Iterable)이 어떤 의미인지 살펴보고 어떻게 객체(Object)를 반복할 수 있게 만들 수 있는지 알아봅시다. 이 글 마지막에서, 여러분은 for-of 루프를 사용자 정의 객체에 어떻게 적용하는지 알게될 것입니다. 그리고 이제 myFavouriteAuthors를 반복 가능(Iterable)하게 만들어봅시다.

반복 가능한 것(Iterable)과 Iterator

이전 섹션에서 우리에게 어떤 문제가 발생했는지는 알았을 것입니다. 사용자 정의 객체에서 'author'에 들어있는 목록들을 가져올 수 있는 쉬운 방법이 없었습니다. 내부 데이터를 차례로 밖으로 노출시키는 방법을 통한 어떤 메소드를 원합니다.

메소드 myFavouriteAuthors 내부에 getAllAuthors 메소드를 추가해봅시다. 이 메소드는 모든 author를 반환할 것입니다. 다음과 같이요.

getAllAuthors.png

getAllAuthors 구현

위에 나온 구현 방법은 정말 간단한 접근법입니다. 모든 author를 가져오긴 합니다. 하지만, 아직 몇가지 문제들이 이 구현된 코드 속에 남아있습니다. 몇가지는 다음과 같습니다.

  • getAllAuthors는 매우 제한적입니다. 만일 어떤 사람이 myFavouriteAuthors를 만든다면, 그 사람은 이 메소드의 이름을 retrieveAllAuthors로 바꿀 것입니다.
  • 개발자로서 우리는 항상 모든 데이터를 반환하는 정확한 메소드를 알아둘 필요가 있습니다. 이 경우에는, getAllAthors라는 이름의 메소드입니다.
  • getAllAuthors는 모든 author의 문자열의 배열을 반환합니다. 만일, 다른 개발자가 오브젝트를 다음과 같은 포맷으로 반환하면 어떻게 할까요?
[ {name: 'Agatha Christie'}, {name: 'J. K. Rowling'}, ...]

개발자는 반드시 모든 데이터를 반환하는 메소드의 정확한 이름과 반환 타입을 알아야 할 것입니다.

만일, 우리가 메소드의 이름과 반환 타입이 고정되어 있고 변하지 않는다는 규칙을 만들면 어떨까요?

이 메소드를 바로 iteratorMethod라고 합니다.

이와 비슷하게 사용자 정의 오브젝트를 반복하는 프로세스의 표준화가 ECMA에 의해 진행되었습니다. 하지만, iteratorMethod라는 이름을 사용하는 대신에, ECMA는 Symbol.iterator라는 이름을 사용했습니다. Symbol은 유일(unique)한 이름을 제공합니다. 그리고 다른 프로퍼티 이름과 충돌이 발생하지 않습니다. 또한, Symbol.iteratoriterator라 불리는 오브젝트를 반환합니다.iteratornext라 불리는 메소드를 가질 것입니다. iterator는 또한 valuedone이라는 키를 가진 오브젝트입니다.

value 키는 현재의 값을 포함할 것입니다. 이 값은 어떠한 타입이든 될 수 있습니다. doneboolean입니다. done은 모든 값이 전달(fetched)되었는지 아닌지를 나타냅니다.

아래의 그림이 iterables, iterators, next 사이의 관계를 알아보는데 도움을 줄 수 있습니다. 이 관계를 Iteration Protocol(반복 프로토콜)이라고 부릅니다.

iteration1.png

이 그림은 Dr Axel Rauschmayer의 책 Exploring JS에 나오는 내용입니다.

  • iterable은 자신의 원소들이 외부에서 접근 가능하도록 만들길 원하는 자료 구조입니다. 키가 Symbol.iterator인 메소드를 구현함으로써 원소들이 외부에서 접근 가능하도록 만듭니다. Symbol.iterator 메소드는 iterator를 위한 공장이라고 보면 됩니다. iterator들을 만들어냅니다.
  • iterator는 자료 구조의 원소들을 순회할 수 있는 포인터입니다.

오브젝트를 반복 가능(iterable)하게 만들어보기

이전 섹션에서 배웠듯이, 우리는 Symbol.iterator 라 불리는 메소드를 구현할 필요가 있습니다. computed property 문법을 이용하여 키를 설정해봅시다. 아래에 짧은 예제가 있습니다.

iterator2.png

iterator의 예제

4번째 줄에서, 우리는 iterator를 만들었습니다. next 메소드가 정의된 오브젝트입니다. next 메소드는 step 변수에 따라 값을 반환합니다. 25번째 줄에서 우리는 iterator를 가져옵니다. 27번째 줄에서, 우리는 next를 호출합니다. donetrue가 될 때까지 next를 계속하여 호출합니다.

이게 바로 for-of 반복 내부에서 일어나는 일입니다. for-of 반복은 iterable한 것을 인자로 받아서 iterator로 만듭니다. 그리고 next()메소드를 done이 true가 될 때까지 호출합니다.

자바스크립트 내에서 Iterable들

Iterable은 영어단어로 반복 가능한이란 뜻을 가지는 형용사가 될 수 있지만, 여기서는 일종의 고유명사로 생각하셔야 합니다. Iterable이란 자체가 반복 가능한 어떠한 객체정도로 생각해주세요.

자바스크립트에서 많은 것들이 반복 가능합니다. 즉시 보이진 않을 수 있어도, 면밀하게 조사해보면, iterable이 보이기 시작합니다.

다음에 나열된 것들은 Iterable입니다.

  • 배열타입이 정해진 배열
  • 문자열 - 각 문자 또는 유니코드 코드-포인트를 반복합니다.
  • - 키-값 쌍을 반복합니다.
  • - 원소를 반복합니다.
  • arguments - 함수의 배열과 같은 특별한 변수를 반복합니다.
  • DOM 원소들

자바스크립트에서 iterables을 인자로 사용하는 생성자들은 다음과 같습니다.

  • for-of 반복 - for-of 반복은 반복 가능한 것을 필요로 합니다. 반복이 불가능하다면, for-ofTypeError를 던질 것입니다.
for (const value of iterable) { ... }
  • 배열의 비구조화 (Destructuring of Arrays) - 비구조화는 반복 가능(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 // Since it was skipped, so it's not assigned
const third = iterator.next().value
iterator.next().value // Since it was skipped, so it's not assigned
const last = iterator.next().value
  • 전개 연산자 (Spread operator)

다음 코드를 봅시다.

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 역시 Promise 전반에서 iterable을 수용합니다.

  • Map과 Set

Map의 생성자는 Iterable [key, pair]의 쌍을 Map으로 변화시킵니다. 그리고 Set의 생성자는 Iterable의 [key, pair]의 쌍을 Set으로 변화시킵니다.

const map = new Map([[1, 'one'], [2, 'two']]);
map.get(1);
// one

const set = new Set(['a', 'b', 'c']);
set.has('c')
// true
  • Iterator는 또한 Generator의 선배입니다.

myFavouriteAuthors를 Iterable로 만들기

myFavouriteAuthors가 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() {
        // 현재 장르 인덱스에 따른 작가들
        const authors = genres[currentGenreIndex];

        // doNotHaveMoreAuthors 는 Authors 배열을 전부 돌았을 때, 참이 됩니다.
        // 모든 아이템이 소비되었을 때, 참이 됩니다.
        const doNotHaveMoreAuthors = !(currentAuthorIndex < authors.length);
        if (doNotHaveMoreAuthors) {
          // 더 이상 불러올 작가가 없을 때, 다음 장르 인덱스(currentGenreIndex)가 다음으로 넘어갑니다.
          currentGenreIndex++;
          // 그리고 작가 인덱스(currentAuthorIndex)가 0이 됩니다.
          currentAuthorIndex = 0;
        }

        // 만일 모든 장르가 끝났다면,
        // 우리는 아이터레이터에게 더 이상 우리가 줄 값이 없다는 것을 알려야 합니다.
        const doNotHaveMoreGenres = !(currentGenreIndex < genres.length);
        if (doNotHaveMoreGenres) {
          return {
            value: undefined,
            done: true
          };
        }

        // 만일 모든 것들이 맞다면, 현재 장르로부터 작가 이름을 반환합니다.
        // 그리고 작가 인덱스를 하나 늘립니다(increment).
        // 다음에는, 다음 작가가 반환됩니다.
        return {
          value: genres[currentGenreIndex][currentAuthorIndex++],
          done: false
        }
      }
    };
  }
};

for (const author of myFavouriteAuthors) {
  console.log(author);
}

console.log(...myFavouriteAuthors);

이번 Iterator 설명 글에서 얻은 지식으로 여러분은 iterator가 어떻게 작동하는지 쉽게 알 수 있었을 것입니다. 로직은 따라가기 힘들 수도 있습니다. 그래서, 코드에 주석을 열심히 달아놓았습니다. 하지만 코드와 개념을 진정으로 이해하려면 브라우저나 노드에서 직접 코드를 실행해보시기 바랍니다.