이터러블과 이터레이터

Crowwan·2023년 1월 4일
post-thumbnail

이터러블은 무엇일까?

이전 함수형 프로그래밍 포스팅에서 for...of문에 대해 설명했다. 가장 중요했던 것은 for...of문은 배열을 순회하기 위한 것이 아니라 이터러블을 순회하기 위한 것이라는 점이다. 그렇다면, 이터러블은 무엇일까?

for...of

우선 for...of문을 순회할 수 있는 값들을 살펴보자.

const arr = [1,2,3]
const set = new Set([1,2,3])
const map = new Map([['a',1],['b',2],['c',3]]);

for(const a of arr) console.log(a);
// 1 2 3

for (const a of set) console.log(a);
// 1 2 3

for (const a of map) console.log(a);
// ['a',1] ['b',2] ['c',3]

위의 코드를 보면 배열과 set, map이 모두 for...of문으로 순회하고 있다. 즉, 배열을 포함해서 Set, Map으로 만든 자료형도 이터러블이라는 것이다. 여기서 for...of문이 내부적으로 일반 for문처럼 인덱스로 접근해서 값을 가져오는 것이 아닌가 하는 의문이 들 수 있다.

그렇다면, 저 데이터를 배열처럼 접근하지 못한다고 가정하면 위의 의문은 해결된다. 배열처럼 접근하지 못한다면, 그러니까 []를 이용하지 못한다는 것은 for...of문이 일반 for문처럼 데이터를 인덱스로 접근해서 순회하는 방식이 아니라는 소리일 것이다. 그럼 확인해보자

console.log(set[0]);
// undefined

위의 코드를 실행해보면 먼저 선언되었던 Set자료형의 데이터를 배열처럼 접근했지만, 값을 얻지 못하고 undefined가 반환되는 것을 확인할 수 있다. 즉, Set을 통해 만든 데이터는 배열처럼 요소를 접근하는 것이 안된다는 것이다.

이런 면에서 봤을 때 for...of문으로 순회하는 것이 일반 배열을 순회하는 것처럼 인덱스를 이용하지 않는다는 것을 알게 되었다.

이터러블

그렇다면 for...of문은 어떤 값을 순회하는 것일까. 바로 이터러블인 값을 순회하는 것이다. 이터러블은 그럼 무엇일까?

이터러블은 아래의 조건을 갖춰야 한다.

  • 이터레이터를 리턴하는 [Symbol.iterator]()를 가진 값

이터러블이라는 것은 위에 말했듯이 이터레이터를 리턴하는 특정 함수가 존재한다는 것이다. 그럼 정말 이터러블이 저런 값을 가지고 있는지 확인해보자.

위 사진은 이전에 사용한 arr변수를 이용했다. 특정 함수나 값을 가졌다는 것은 객체에서 프로퍼티 접근 방식으로 접근이 가능하다는 의미이다. 프로퍼티를 접근하는 방법은 .[]이 있다. .을 사용하기 위해서는 그 프로퍼티의 이름이 네이밍 컨벤션을 지키고 있어야 하지만, Symbol.iterator라는 것은 네이밍 컨벤션이 아닌 Symbol의 프로퍼티를 참조하는 방식이므로, []를 사용해서 접근이 가능하다.

이런 식으로 접근했을때, 결과를 보면 Array Iterator가 반환된 것을 볼 수 있다.. 앞서 조건을 설명했던 것처럼 이 프로퍼티는 이터레이터를 리턴한다.

이터레이터

이터러블은 이터레이터를 리턴하는 [Symbol.iterator]()를 가지고 있어야 한다는 것을 알았다.
그렇다면 이터레이터는 또 무엇인가?

이터레이터는 다음과 같은 조건을 갖춘다.

  • { value, done }을 키로 가지는 객체를 리턴하는, next()를 가진 값

이터레이터가 어떤 모습을 하고 있는지 확인해보자.

위 사진은 이터레이터를 iter에 받아 내부를 확인한 결과이다. 보면 알 수 있듯이 next라는 메소드가 존재한다는 것을 알고 있다. 그리고 이터레이터는 {value,done}을 리턴한다고 했으면, 함수가 아닌가 하고 함수처럼 호출을 해봤지만, 오류가 나는 모습이다.

이터레이터는 값을 직접 리턴하는 것이 아니다. 그렇다면 여기서 값을 리턴할 수 있는 것은 무엇일까? 사실 답은 하나이다. 바로 next라는 메소드다. 메소드는 함수이다. 함수라는 것은 특정 값을 반환할 수 있다는 것이된다. 그리고 그 반환되는 값이 이 경우에는 {value,done}형태라는 것이다.

실제로 확인해보면, next()메소드는 위와 같은 값을 반환한다.

이터레이터의 동작방식은 순회를 해야하는 순간이 온다면, []를 이용해서 순회를 하는 것이 아닌 next()를 이용해서 값을 순회한다.
그렇다. value는 이터러블한 객체의 특정 값이 되는 것이고, done은 이 객체의 순회가 끝났는지를 나타낸다.


위 사진을 보면 알 수 있을 것이다. next메소드를 통해 얻은 객체에 value에 접근하여 값을 얻고 만약 donetrue가 되었다면 순회를 종료하는 방식이다.

이터러블과 이터레이터 그리고 for...of

그렇다면, 이터러블은 이터레이터를 가진다고 했는데, 실제 for...of문은 어떻게 동작할까?
for...of문은 이터러블을 순회하기 위한 문이라고 했다. 그럼 왜 이터러블만 순회를 하는 것일까?

for...of문은 이터러블에서 [Symbol.iterator]()를 통해 이터레이터를 얻어 next메소드를 호출하면서 값을 얻고 donetrue일 때 종료하는 방식이다. 그런데 아래 코드를 보자.

const arr = [1,2,3]
const iter = arr[Symbol.iterator]();

for(const a of iter) console.log(a);
// 1 2 3

위 코드를 보면 배열을 반복문에 넘겨주는 것이 아닌 이터레이터를 넘겨주고 있다. 그런데도 같은 동작을 한다. 그렇다면 for...of문은 이터러블을 순회하는 것이 아니라 그냥 이터레이터를 순회하는 것 아닐까?

답을 말하자면 그렇지 않다. 이에 대해서는 밑에서 설명하겠다.

웰 폼드 이터레이터(이터러블)

이터러블은 개발자가 직접 생성할 수 있다. 아래의 코드를 보자

const iterable = {
  [Symbol.iterator](){
    let i = 3
    return {
      next(){
        return i === 0 ? {done: true} : {value:i--, done:false}
      }
    }
  }
}


const iterator = iterable[Symbol.iterator]();
iterator.next().value;
// 3
  • 먼저 이터러블은 객체이고, [Symbol.iterator]를 가진다.

  • 이것을 호출했을 때 이터레이터를 반환하는데, 이터레이터는 내부에 next메소드를 가지고 있다.

  • next메소드는 i가 0이 아닐 경우 { value: i, done: false }를 반환한다.

  • i가 0일 경우 { done: true }를 반환한다.

실제로 이터레이터를 변수에 담아 next메소드를 호출하여 value를 출력하면 첫 i의 값인 3을 얻을 수 있다. 여기서 중요한 것은 { done:true }를 비교해서 반복을 종료하는 것은 이터레이터가 하는 것이 아니라 순회를 하는 for...of에서 내부적으로 판단한다는 것이다.

다시 돌아가서, 위와 같은 식으로 직접 이터러블을 만들 수 있지만, 이런 이터러블 중에서도 웰 폼드 이터러블이라는 형태가 있다.

이는 이터러블에서 얻은 이터레이터도 [Symbol.iterator]를 가진 이터러블이면서 거기서 얻은 이터레이터는 자기 자신을 반환한다는 것이다. 무슨 뜻인지 이해가 가지 않겠지만 차근차근 확인해보자.

const map = new Map([['a',1],['b',2]]);

for(const a of map.keys()) console.log(a);

// a b

위 코드를 실행하면 map의 키를 순회할 수 있다. 즉, map.keys()라는 메소드 호출이 반환하는 것이 이터러블이라는 것이다.

이것을 확인하기 전에 위에서 우리가 하나 넘어갔던 것을 돌이켜보자.

const arr = [1,2,3]
const iter = arr[Symbol.iterator]();

for(const a of iter) console.log(a);
// 1 2 3

이 코드였다. for...of문이 이터러블이 아닌 이터레이터를 순회하는 것이 아닌가 하는 의문이다. 이는 아까 우리가 만든 이터러블 객체를 통해 답을 얻을 수 있다.

이터러블 객체를 만드는 코드에서 이터레이터를 iterator에 저장했다. 그럼 이것을 for...of문에 넣게 된다면, 그리고 동작을 하게 된다면, for...of문은 이터레이터를 받아서 순회할 수 있다는 의미이다.

하지만 우리가 생각했던 것처럼 동작하지 않는다. 오류를 보면 iterator is not iterable이라는 문구가 보인다. 즉, for...of문은 이터러블만을 받는 것이고 우리가 만든 이터러블의 이터레이터는 이터러블이 아닌 이런 꼬리에 꼬리를 무는 무서운 이야기가 완성된다.

그렇다면 위에서 저장한 iter라는 이터레이터는 왜 for...of문을 순회할 수 있을까? 이 의미는 iter라는 이터레이터 즉, 배열에서 [Symbol.iterator]()를 통해 얻는 이터레이터는 이터러블이라는 것이다.

다시 이터러블은 [Symbol.iterator]()를 가진다고 했다.

사진으로 비교해보자. iter는 배열의 이터레이터고 iterator는 우리가 만든 이터러블 객체의 이터레이터다. 내용을 보았을 때 iter[Symbol.iterator]()를 가지고 있다는 것을 알 수 있다. 즉, 이터러블이다. 하지만 우리가 만든 이터레이터는 그 값이 없다. 즉, 이터러블이 아니라는 의미이다.

여기서 정리할 수 있는 것은 for...of문은 이터러블만을 순회한다는 것이고, 이터레이터는 항상 이터러블이 아니라는 것이다.

그렇다면 이터레이터를 이터러블로 만들면 되지 않을까? 맞다. 이것이 바로 웰 폼드 이터러블이다.

즉, 이터러블 중에서 이터레이터가 똑같이 이터러블인, 또 그 이터레이터의 이터레이터는 자기 자신을 나타낼 때 이를 웰 폼드 이터레이터라고 한다.


그러니까 우리가 만든 이터러블을 웰 폼드 이터러블로 수정해보면,

const iterable = {
  [Symbol.iterator](){
    let i = 3
    return {
      next(){
        return i === 0 ? {done: true} : {value:i--, done:false}
      },
      [Symbol.iterator](){ return this }
    }
  }
}


const iterator = iterable[Symbol.iterator]();
iterator.next().value;
// 3

위와 같은 형태가 될 것이다.

profile
어렵게 하는 개발 공부

0개의 댓글