[ES6] Iterable-Iterator 란?

Yoon Han·2022년 9월 4일
0
post-thumbnail

Symbol.iterator ?

Iterable 과 Iterator 에 관해 이야기하기 전에 Symbol 자료형을 먼저 짚고 넘어갈 필요가 있어요.

Symbol ES6 에서 새로 생겨난 자료형인데요, MDN문서 를 읽어보면.. 이해가 되지 않는 말들로 가득합니다만 제 나름대로 핵심만 뽑아서 정리해보겠습니다.

Symbol익명의 고유한 값을 만들어낸다.

'익명' 이라는 것은 변수처럼 이름이 붙지 않는다는 것이고(물론 Symbol 값을 변수에 할당하면 이름이 붙는 셈이지만요),
'고유한' 값이라는 것은 다른 모든 값들과 구분되는 값이라는 뜻이에요. 심지어 다음도 성립합니다.

Symbol('abc') !== Symbol('abc')

그래서 Iterable 이란?

Iterable 과 Iterator 개념은 ES6 에 새로 도입된 개념이에요.
우선 Iterable 부터 알아보도록 할게요.

Iterable은 '반복 가능한 객체' 정도로 받아들이면 되는데, 이게 무엇인지에 관해서는 다음 사항만 알고 있으면 돼요.

Iterable === ([Symbol.iterator] 프로퍼티를 가진 객체)

즉, 다음과 같은 형식을 가지고 있어요.

const iterable = {
  [Symbol.iterator]() {
	return ...
  }
}

코드를 보면 Symbol.iterator 프로퍼티의 값은 함수(인자가 없는)인 것을 알 수 있어요.
그럼 이 함수는 어떤 값을 반환하고 있는 걸까요?

Iterator 는 무엇?

위에서 Iterable 객체는 Symbol.iterator 프로퍼티를 가진다는 것을 알게 되었어요.
Iterator 란 이 Symbol.iterator 프로퍼티에 할당된 함수를 실행하면 반환되는 또 다른 객체라고 보면 됩니다.

Iterator === (Iterable 의 Symbol.iterator 메서드에서 반환되는 객체)

물론 아무 객체나 Iterator 가 될 수 있는 것은 아니에요.
다음과 같은 조건을 만족시켜야 합니다.

  1. next 메서드를 가지고 있어야 한다.
  2. next 메서드는 value, done 프로퍼티를 가진 객체를 반환해야 한다.

즉, iterator 는 다음과 같은 형식이어야 한다는 뜻이에요.

const iterator = {
    next() {
      return { value: 0, done: true }
    },
    
    [Symbol.iterator]() {
    	return this
	}
}

위의 코드를 보면 iterator 변수에 담긴 객체에 두 가지 메서드가 있는것을 볼 수 있습니다.

하나는 next 메서드 입니다. valuedone 프로퍼티를 가진 객체를 반환하죠.

다른 하나는 Symbol.iterator 메서드인데, iterator 가 well-formed iterator 로 진화하기 위해서는 이 메서드를 꼭 가지고 있어야 합니다. 이 메서드가 하는 일은 단지 그냥 iterator 자기 자신을 반환하는 것 뿐이에요.

next 메서드가 반환하는 객체에 포함된 valuedone 프로퍼티에 관해서는 뒤에 예시를 통해 설명드리도록 하겠습니다.

Iterable 과 Iterator 합치기

이제 앞서 설명한 두 가지(iterable, iterator)를 합쳐서 코드로 표현해보도록 할게요.

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

하나씩 살펴보겠습니다.
iterable 변수에 담긴 값은 Symbol.iterator 메서드를 가지고 있는 객체네요.

이 메서드는 iterator 객체를 반환하는데, 이 객체는 또 next 메서드를 가지고 있구요.
next 메서드는 클로져 변수인 i 의 값에 따라 valuedone 값이 다른 객체를 반환하고 있습니다.

value 프로퍼티는 iterator 객체가 반환하는 객체의 '값' 이고,
done 프로퍼티는 iterator 객체의 순회가 끝났는지 여부를 나타냅니다. 즉, 이 값이 true 라면 더 이상 iterator 를 순회하지 않아요.

위 코드를 for ... of 문과 함께 실행시켜 볼게요.

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

for (let item of iterable) {
    console.log(item)
}

위 코드를 실행 시켜보면 다음과 같은 결과가 나옵니다.

iterable 변수에 담긴 객체가 Symbol.iterator 메서드를 통해 iterator를 반환하므로 for ... of 구문에 사용할 수 있는거에요.

for ... of 구문 뿐만 아니라 전개 연산자(spread operator, ...), 구조분해 할당(destructuring, [a, b, c, ...rest])문법에도 쓰일 수 있죠.

반복문 놔두고 왜 Iterator 를 쓸까요?

아무리 좋은 지식도 용도를 모르면 알고 있다고 해도 나중에 써먹기가 힘든게 사실입니다. (저도 사실 프론트엔드 실무하면서 위와 같은 구문을 써본 일은 없어요...ㅎㅎ)

그래서 혹여 나중에 필요할 일이 생길 경우를 대비에 iterator 개념의 활용 범위만큼은 알고 가야 한다고 생각합니다.

iterator 의 장점을 한 가지만 생각해보라고 한다면 저는 상태를 저장해두었다가 언제든 다시 꺼내쓸 수 있는 반복문 이라고 생각해요.

보통 다음과 같이 for 문을 작성하면 중간 상태를 저장해두지 않고 처음부터 끝까지 순회를 하게 되죠.

for (let i = 0; i <= 10; i++) {
  console.log(i)
}

// 0
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
// 10

그런데 똑같은 반복문을 iterator 로 구현하게 되면, 다음과 같이 내가 필요할 때만 순회하는 것이 가능해요.

// Well-formed iterator 를 가지는 iterable
const iterable = {
  [Symbol.iterator]() {
    let i = 0
    return {
      next() {
        return i <= 10 ? { value: i++, done: false } : { value: i++, done: true }
      },

      [Symbol.iterator]() {
        return this
      }
    }
  }
}

const iterator = iterable[Symbol.iterator]()
console.log(iterator.next()) // { value: 0, done: false }
//
// 다른 작업 1 수행...
//
console.log(iterator.next()) // { value: 1, done: false }
//
// 다른 작업 2 수행...
//
console.log(iterator.next()) // { value: 2, done: false }

마치 커스터마이징된 반복문을 쓰는 것 같은 느낌이 드네요.

이번 포스트를 통해, 나중에 필요한 곳에 사용하면 굉장히 유용할 iterator 개념을 소개해 드렸습니다!

직접 iterator 를 구성해보시면서 개념에 익숙해지도록 해보시면 좋을것 같습니다 ㅎㅎ


레퍼런스

profile
챗바퀴는 도는 삶이 싫은 프론트엔드 엔지니어

0개의 댓글