Iterable 과 Iterator 에 관해 이야기하기 전에 Symbol
자료형을 먼저 짚고 넘어갈 필요가 있어요.
Symbol
ES6 에서 새로 생겨난 자료형인데요, MDN문서 를 읽어보면.. 이해가 되지 않는 말들로 가득합니다만 제 나름대로 핵심만 뽑아서 정리해보겠습니다.
Symbol
은 익명의 고유한 값을 만들어낸다.
'익명' 이라는 것은 변수처럼 이름이 붙지 않는다는 것이고(물론 Symbol
값을 변수에 할당하면 이름이 붙는 셈이지만요),
'고유한' 값이라는 것은 다른 모든 값들과 구분되는 값이라는 뜻이에요. 심지어 다음도 성립합니다.
Symbol('abc') !== Symbol('abc')
Iterable 과 Iterator 개념은 ES6 에 새로 도입된 개념이에요.
우선 Iterable 부터 알아보도록 할게요.
Iterable은 '반복 가능한 객체' 정도로 받아들이면 되는데, 이게 무엇인지에 관해서는 다음 사항만 알고 있으면 돼요.
Iterable === ([Symbol.iterator] 프로퍼티를 가진 객체)
즉, 다음과 같은 형식을 가지고 있어요.
const iterable = {
[Symbol.iterator]() {
return ...
}
}
코드를 보면 Symbol.iterator
프로퍼티의 값은 함수(인자가 없는)인 것을 알 수 있어요.
그럼 이 함수는 어떤 값을 반환하고 있는 걸까요?
위에서 Iterable 객체는 Symbol.iterator
프로퍼티를 가진다는 것을 알게 되었어요.
Iterator 란 이 Symbol.iterator
프로퍼티에 할당된 함수를 실행하면 반환되는 또 다른 객체라고 보면 됩니다.
Iterator === (Iterable 의
Symbol.iterator
메서드에서 반환되는 객체)
물론 아무 객체나 Iterator 가 될 수 있는 것은 아니에요.
다음과 같은 조건을 만족시켜야 합니다.
next
메서드를 가지고 있어야 한다.next
메서드는value
,done
프로퍼티를 가진 객체를 반환해야 한다.
즉, iterator 는 다음과 같은 형식이어야 한다는 뜻이에요.
const iterator = {
next() {
return { value: 0, done: true }
},
[Symbol.iterator]() {
return this
}
}
위의 코드를 보면 iterator
변수에 담긴 객체에 두 가지 메서드가 있는것을 볼 수 있습니다.
하나는 next
메서드 입니다. value
와 done
프로퍼티를 가진 객체를 반환하죠.
다른 하나는 Symbol.iterator
메서드인데, iterator 가 well-formed iterator 로 진화하기 위해서는 이 메서드를 꼭 가지고 있어야 합니다. 이 메서드가 하는 일은 단지 그냥 iterator 자기 자신을 반환하는 것 뿐이에요.
next
메서드가 반환하는 객체에 포함된 value
와 done
프로퍼티에 관해서는 뒤에 예시를 통해 설명드리도록 하겠습니다.
이제 앞서 설명한 두 가지(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
의 값에 따라 value
와 done
값이 다른 객체를 반환하고 있습니다.
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 의 장점을 한 가지만 생각해보라고 한다면 저는 상태를 저장해두었다가 언제든 다시 꺼내쓸 수 있는 반복문
이라고 생각해요.
보통 다음과 같이 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 를 구성해보시면서 개념에 익숙해지도록 해보시면 좋을것 같습니다 ㅎㅎ