항상 모든 것에는 "왜 탄생되었는지"를 이해하는 것이 수반되어야 합니다.
우리, 그냥 단순히 받아들이지 말고 생각을 해봅시다.
💡 왜 이터러블이 탄생되었을까요?
우리, 유사 배열 객체라는 것을 기억하나요?
length
를 가지고 있으면서, 마치 배열처럼 구성되어 있는 객체를 의미했죠!
그런데, 이 친구들 문제가 있어요 😖
바로 순회가 불가능하다는 점이었습니다.
하지만 우리의 개발자분들! 이를 순회 가능하게 만들었어요.
이를 forEach
등으로 메서드를 사용하여 순회가 가능하게 만들었는데요.
😱 문제는, 이러한 순회가 표준화되어 있지 않았다는 점이었어요...
이러한 현상이 반복되다 보면, 결과적으로 앞으로의 자료구조나, 다른 라이브러리를 참고할 때 굉장히 많은 혼란이 발생하겠죠?
따라서 이터러블은 이러한 맥락에서 탄생되었습니다.
💡 "우리, 이런 것들 잘 지켜준 객체들은 적어도 일관성 있게 순회 가능하게 해주자!"
결과적으로, 순회에서는 좀 더 클린한 자바스크립트 세상이 탄생한 순간이었겠군요. 🎉
프로토콜은 약속이죠. 따라서 이터레이션은 자료구조에 있어 순회에 대한 일련의 규칙입니다.
이는 물론 ECMAScript
사양을 근거하여 정의되어 있는데요.
우리는 크게 2가지를 기억하면 됩니다.
이 친구들을 합해서 이터레이션 프로토콜을 구성하기 때문입니다.
이름이 비슷하여 어려워 보이지만, 막상 이해하면 그렇게 어렵지 않습니다.
이터러블은 무슨 뜻일까요?
iterate
는 반복하다라는 의미니까... 음, 반복 가능한의 의미려나요? 🙇🏻♂️
맞습니다. 결국 반복문이 가능하다는 의미겠죠?
💡 그렇다면, 이터러블 프로토콜은, 이를 지켜야만 반복문 수행이 가능하다는 의미를 갖게 되겠군요!
따라서 이를 너무 syntactic하게 외우는 것은 오히려 혼동스럽습니다.
오히려, context로 이해하는 게 더 편한데요.
이터러블 프로토콜은 결국 반복문(for ... of ...
)의 조건을 이야기하는 거라 생각하면 됩니다. 그리고 이를 준수하면 우리는 그 객체를 이터러블이라고 얘기를 합니다.
그렇다면, 그 조건은 뭘까요?
핵심은 바로, 이전 챕터에서 설명했던 'Well-known Symbol'인 Symbol.iterator
입니다.
Symbol.iterator
프로퍼티 키로 정의한 메서드가 있는가.해당 프로퍼티 키를 가진 메서드가 있다면, 우리는 그 객체를 이터러블이라 정의할 수 있습니다.
const iterableObject = {
0: 1,
1: 2,
2: 3,
3: 4,
4: 5,
[Symbol.iterator]() {
let cur = 0;
const max = Object.keys(iterableObject).length;
return {
next() {
console.log('test')
return { value: iterableObject[cur], done: cur++ === max };
}
};
}
};
// 이제는 객체 순회를 통해 값에 접근할 수 있군요! 😮
for (let i of iterableObject) {
console.log(i)
}
// 1
// 2
// 3
// 4
// 5
Symbol.iterator
메서드를 상속 받았는가.대표적으로 배열이 있겠죠.
배열은 Array.prototype
을 상속받고, Array.prototype
에는 [Symbol.iterator]
로 정의된 메서드가 내장되어 있습니다. 따라서 배열은 이터러블입니다.
const arr = [1,2,3,4];
// arr은 Array를 상속받았으므로 Symbol.iterator 프로퍼티가 존재한다.
console.log(Symbol.iterator in arr); // true;
// Array의 [[Prototype]]에는 Symbol.iterator 프로퍼티 메서드가 존재한다.
console.log(Symbol.iterator in arr.__proto__); // true;
💡 책으로 보니, 이터러블 객체를 판단할 수 있는 유틸 함수를 만드셨군요!
const isiterable = v => v !== null && typeof v[Symbol.iterator] === 'function'
그럼 이제, 이터레이터 프로토콜을 살펴볼 건데요. 이터레이터는 Symbol.iterator
에 들어간 메서드에 관한 프로토콜이라고 이해하면 쉬울 것 같아요! 🥰
이 친구는 아까의 예시를 갖고와서 설명해볼까 해요.
const iterableObject = {
0: 1,
1: 2,
2: 3,
3: 4,
4: 5,
[Symbol.iterator]() {
let cur = 0;
const max = Object.keys(iterableObject).length;
// 이 부분이 이터레이터 프로토콜을 따른 객체입니다!
return {
next() {
console.log('test')
return { value: iterableObject[cur], done: cur++ === max };
}
};
}
};
해당 리턴 값의 객체가 보이시나요?
이 객체는 next
라는 메서드를 갖고 있는데요!
이 next
의 리턴값에는 value
와 done
이 있네요.
참고로, 이 next
메서드의 리턴값을 우리는 이터레이터 리절트 객체라 부른답니다.
이는 다음을 의미하고 있어요.
next()
: 다음에 반환되는 것이 무엇인지를 정의해요.value
: 현재의 값을 의미해요.done
계속해서 순회할 때, 언제 순회를 끝낼지를 판단하기 위한 조건식을 정의해요.네, 이터레이터의 조건은 이 3가지를 만족하면 된답니다.
이렇게 생각하니 어렵지 않죠? 😆
const iterable = '1234'; // String 래퍼 객체도 이터러블입니다.
const iterator = iterable[Symbol.iterator]();
console.log(iterator.next()) // {value: '1', done: false}
console.log(iterator.next()) // {value: '2', done: false}
console.log(iterator.next()) // {value: '3', done: false}
console.log(iterator.next()) // {value: '4', done: false}
console.log(iterator.next()) // {value: undefined, done: true}
아무래도 이름이 워~낙 헷갈려서 한 번 더 정리해보는 게 우리의 기억에 도움이 될 것 같네요.
핵심은 다음과 같아요.
Symbol.iterator
프로퍼티 키를 갖고 있느냐로 판단된다.Symbol.iterator
메서드에 mapping된 함수가 잘 정의되어 있는지에 대한 규약이다.next
메서드와, next
메서드가 이터레이터 리절트 객체(value
와 done
)을 잘 구성했는지로 준수 여부를 판단한다.자, 이제 이해가 되었나요?
그러나 우리의 여정은 아직 끝나지 않았으니, 좀 더 살펴보도록 하죠! 🔥
쉽게 말하자면, 내장 빌트인 객체 중에 어떤 것들이 이터러블한지에 대한 설명이 책에 들어있군요!
Array
String
Map
Set
TypedArray
(이 친구는 어려울 수 있는데, 타입과 버퍼에 따른 값을 넣을 수 있도록 하는 배열 인스턴스를 만드는 객체의 프로토타입 객체에요.)arguments
DOM Collection
for ... of ...
결국 이터러블이 이터러블하게 동작할 수 있도록 하는 핵심인데요!
이터러블 순회에 관한 문법입니다.
for ... in ...
가끔 이와 헷갈리시는 분들을 봤었어요.
핵심은, 결국 이터러블하냐가 맞습니다.
다만, for ... in ...
은 객체의 프로토타입 체인에 존재하는 프로퍼티들 중, [[Enumerable]]: true
인 친구들과 Symbol
프로퍼티가 아닌 프로퍼티들의 키를 열거하는 친구에요.
그러나 우리의 for ... of ...
는 뭐라고 했죠? 이터러블 객체의 순회를 위한 친구라 했죠! 따라서 객체는 이터러블하지 않으면 작동하지 않습니다.
또한 Symbol.iterator
에서의 리절트 객체의 value
는 프로퍼티 값이 할당되어 있고, done
으로 조건을 판단해요. 이 과정에서 좀 더 표준화된 결과값을 출력하게 되어 있는데요.
따라서 for ... of ...
는 유효한 프로퍼티 값을 순회하게 된답니다 😉
a = [1,2,3,4]
a.foo = () => {};
for (let i of a) { console.log(i) } // 1 2 3 4
for (let i in a) { console.log(i) } // 0 1 2 3 foo
++ 추가적으로 제가 알기로는,
for ... of ...
는 내부 연산에 있어서 결국 다음 value를 기억할 수밖에 없기도 하고, 모든 프로토타입 체인을 추적하지 않습니다. 따라서 성능이 더욱 우수합니다.
책에 있는 예제를 통해 지연 평가를 살펴 보도록 하죠!
const fibonacciFunc = function() {
// 클로저로 구현해보도록 하죠!
let [pre, cur] = [0, 1];
return {
[Symbol.iterator]() {
return {
next() {
// 순회하면서, pre와 cur을 업데이트합니다.
[pre, cur] = [cur, pre + cur];
// 우리는 끝나지 않는 함수를 만들어볼까 해요.
// 기본적으로 done의 값은 설정되어 있지 않다면 not truthy합니다.
return {
value: cur,
}
}
}
}
}
}
var [a, b, c, d, e] = fibonacciFunc();
console.log(a,b,c,d,e) // 1 2 3 5 8
이처럼, 이터러블의 iterator
를 잘 활용한다면 데이터를 미리 할당하지 않고, 필요할 때에 평가하고 생성해내는 지연 평가가 가능해집니다.
이건 최적화를 위해 사용합니다.
특히, 함수형 프로그래밍에서는 자주 봤던 것 같아요!
보통 함수형 프로그래밍이 느리다고는 하지만, 결국 이러한 지연 평가 등을 통해 메모리를 최적화시키는 기법도 존재해서 정말 잘 사용하면, 성능이 빠른 구현도 가능하다고 합니다.
후우... 이전에 이터러블에 관한 글을 노션에 썼던 기억이 나요.
다시 보고 나니, 그때는 정말 잘 이해하지 못하고 썼다는 느낌이 드네요. 지금에서야 뭔가 제대로 이해하는 느낌이에요.
현재 포트폴리오 웹사이트를 만들고 있는데요, 이것이 끝나면 본격적으로 함수형 프로그래밍을 연습해보려 합니다.
이유는 간단합니다. 재밌어 보이기 때문이죠 😉
굳이 공부에 너무 하나하나의 납득할 만한 배경을 갖고 있지는 않아도 되고,
그렇기 때문에 아직 일부러 지원서도 넣지 않고 있어요.
(실제로 일하면서 원하는 공부를 하기에는, 일을 배우는 입장에서 어렵더라구요)
여튼, 공부하는 이 순간이 굉장히 즐겁네요. 다들 즐거운 공부하시길 바라며, 이상! 🌈