기존의 이터러블 객체 순회
기존의 이터러블 객체(순환이 가능한 객체)는 for i++과 같은 형태로 이터러블 객체를 순환하였다.
const students = ['동규', '소현', '찬진', '요셉'];
for (let i = 0; i < students.length; i++) {
console.log(students[i] + '님은 제 친구입니다.');
}
위와 같이 전체 이터러블 객체를 순환하도록 하였다. 이뿐만 아니라 문자열도 이터러블한 자료형이기 때문에 위와 같이 할 수 있다.
const alphabet = 'abcdef'
for (let i = 0; i < alphabet.length; i++) {
console.log(alphabet[i]);
}
즉, 숫자를 키로 하여 이터러블한 객체를 순회하면서 값을 출력하게 된다.
하지만, ES6에서는 이터러블 객체를 순회할 수 있는 또 다른 방법이 존재하는데, 바로 for of 문을 사용하는 것이다.
const students = ['동규', '소현', '찬진', '요셉'];
for (const student of students) {
console.log(student + '님은 제 친구입니다.');
}
이처럼 for of 문을 사용하면 조금 더 명확하게 리스트 순회를 사용할 수 있다. 이터러블한 것은 모두 적용이 되기 때문에, 문자열 또한 for of 문으로 순회할 수 있다.
const alphabet = 'abcdef'
for (const element of alphabet) {
console.log(element);
}
이터러블? 이터레이터?
자바스크립트에서는 Array와 Set, Map 자료형에 대하여 리스트와 같이 순회할 수 있다.
const arr = [1, 2, 3];
for (const a of arr) console.log(a);
const set = new Set([1, 2, 3]);
for (const a of set) console.log(a);
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
for (const a of map) console.log(a);
이때, Array는 대괄호 안에 키를 넣어서 조회를 할 수 있지만, Set과 Map은 이와 같이 인덱스로 조회가 불가능하다.
즉, for of 문이 기본적으로 기존의 for const로 순회하는 코드와는 다른 형태로 내부적으로 구현이 되어있다는 반증이다.
자바스크립트 ES6에서는 새로운 자료형인 Symbol이라는 자료형이 추가되었다. Symbol은 독립적으로 존재하는 고유 키인데, 만약 Symbol로 프로퍼티를 정의한다면, 그 프로퍼티는 Symbol외에 다른 키워드로 접근이 불가하게 된다.
const alphabet = { a: 'a', b: 'b', c: 'c' };
const d = Symbol();
alphabet[d] = 'd';
Object.keys(alphabet);
> (3) ['a', 'b', 'c']
Symbol은 고유의 정적 속성을 가지는데 이때 Symbol.iterator라는 속성을 가진다. 그리고, Array에서는 이를 키로 사용하여 arr[Symbol.iterator]로 배열에 접근하면 한 함수를 가지고 있음을 확인할 수 있다.
위 arr 배열의 해당 키의 값을 null로 바꿔서 함수를 없애보면 어떻게 될까?
arr[Symbol.iterator] = null;
for (const element of alphabet) console.log(element);
// Uncaught TypeError: arr is not iterable at ...
결과는 arr이 이터러블이 아님을 보여준다. 즉, for of 문과 Symbol.iterator가 일정 부분 연관이 존재하게 되는 것이다. 이는 비단 Array에만 속하는 특징이 아니라, Set, Map도 마찬가지로 해당 Symbol.iterator를 가진다는 것을 의미한다.
이터러블과 이터레이터라는 단어가 계속해서 언급되고 있는데, 앞서 학습한 내용을 충분히 이해하기 위해서 그리고 앞으로의 학습한 내용을 온전히 받아들이기 위해서 이러한 단어들이 어떠한 것을 의미하는 것인지 미리 파악할 필요가 있다.
- 이터러블: 이터레이터를 반환하는 [Symbol.iterator]() 를 가진 값
- 이터레이터: { value, done } 객체를 리턴하는 next() 를 가진 값
- 이터러블/이터레이터 프로토콜: 이터러블을 for of, 전개 연산자 등과 함께 동작하도록 한 규약
앞서 Array와 Set, Map은 이터러블한 객체였다. 즉, 해당 객체들은 내부에 Symbol.iterator를 키로 하며 { value, done } 객체를 리턴하는 next() 를 가지는 이터레이터를 값으로 취하는 것이다. 그렇기에 Array와 Set, Map은 이터러블 프로토콜을 따르고 for of, 전개 연산자를 사용할 수 있는 것이다.
console.log(arr[Symbol.iterator]);
// f values() { [native code] }
const iterator = arr[Symbol.iterator]();
iterator.next();
// {value: 1, done: false}
iterator.next();
// {value: 2, done: false}
iterator.next();
// {value: 3, done: false}
iterator.next();
// {value: undefined, done: true}
iterator.next();
// {value: undefined, done: true}
즉, Array, Set, Map은 자바스크립트의 내장객체로서 이터러블 프로토콜을 따르고 있다고 말할 수 있다. Set도 이터레이터를 뽑아내 next() 메서드로 값을 뽑아낼 수 있고, Map도 keys()나 values(), entries() 메서드를 사용하면 이터레이터를 반환하기 때문에 이터러블한 객체이며, 해당 이터레이터로 순회를 할 수 있다. 그런데 신기하게도, iterator를 Symbol.iterator를 키로 하여 접근하게 되면 값을 가지고 있는 것을 확인할 수 있으며, 각 메서드의 Symbol.iterator를 실행한 결과를 접근을 할 수 있는 것이다.
사용자 정의 이터러블
사용자는 이터러블/이터레이터 프로토콜을 따르는 객체를 만들었을 때, 이터러블의 메서드를 사용할 수 있는 이터러블 객체를 자체적으로 사용할 수 있다.
const iterable = {
[Symbol.iterator]() {
let i = 3;
return {
next() {
return i === 0 ? { done: true } : { value: i--, done: false };
}
}
}
};
let iterator = iterable[Symbol.iterator]();
console.log(iterator.next());
// {value: 3, done: false}
console.log(iterator.next());
// {value: 2, done: false}
console.log(iterator.next());
// {value: 1, done: false}
console.log(iterator.next());
// {value: undefined, done: true}
for (const a of iterator) console.log(a);
// 3
// 2
// 1
위처럼 Symbol.iterator를 키로 하며 값으로 { value, done } 객체를 반환하는 함수를 값으로 가지게 될 경우 이터러블한 객체를 만들어낼 수 있다.
하지만, 아직 완전한 Well formed iterator라고는 할 수 없다.
Array의 경우에는 이터레이터의 next() 함수를 통해 일정 부분 순회를 진행하고, 다음에 다시 순회를 재개했을 때 순회가 완료된 시점에서부터 다시 순회를 한다. 이러한 경우 잘 만들어진 이터레이터가 되는 것이다.
이처럼 이터레이터가 자기 자신을 반환하는 Symbol.iterator를 가지고 있을 때 Well formed iterator, Well formed iterable이라고 할 수 있다.
즉, 위의 코드를 아래의 코드로 변경하면 된다.
const iterable = {
[Symbol.iterator]() {
let i = 3;
return {
next() {
return i === 0 ? { done: true } : { value: i--, done: false };
},
// 이터레이터의 Symbol.iterator가 자기 자신을 반환
[Symbol.iterator]() { return this; }
}
}
};
let iterator = iterable[Symbol.iterator]();
console.log(iterator.next());
// {value: 3, done: false}
console.log(iterator.next());
// {value: 2, done: false}
for (const a of iterator) console.log(a);
// 1
이처럼 이터레이터를 이터러블로 만들면 이터레이터를 사용해서도 순회를 할 수 있다.
Array와 Set, Map 이외에도 ES6에서는 대부분의 이터러블한 내장 객체들이 이터러블하게 변환되었다.
for (const a of document.querySelectorAll('*')) console.log(a);
전개 연산자
전개 연산자는 이터러블 객체에서 사용할 수 있는 연산자이다.
const a = [1, 2];
console.log(...a);
// 1 2
이터러블 프로토콜을 따르는 모든 것은 전개 연산자를 활용할 수 있다.
위처럼, 우리가 단순히 사용하고 있던 함수들과 for문들은 내부적으로 이터러블을 통해 구현되어 있는 것들이다.
이를 알고 프로그래밍 하는 것과 모르고 프로그래밍하는 것은 눈에 보이지 않지만, 매우 큰 차이를 가진다고 생각한다. 내가 이 로직이 어떻게 흘러가며, 왜 이런 결과를 보여주는지 아는 사람과 모르는 사람 간에는 서서히 격차가 발생할 것이다.
각 함수들의 기초를 알고 이 기초를 잘 활용하면, 내 마음 속에 있던 블랙박스를 걷어낼 수 있고, 더 나은 개발을 할 수 있을 것이라 생각한다. 사실 처음에는 이걸 왜 공부하는 것일까? 하는 궁금증이 있었으나 기초를 탄탄하게 가져감으로써 자바스크립트 개발자로서의 내실을 다질 수 있었다.
출처: 함수형 프로그래밍과 JavaScript ES6+(https://www.inflearn.com/course/functional-es6/dashboard)
'프론트엔드 > JS' 카테고리의 다른 글
[JavaScript] 이터러블 프로토콜과 map, filter, reduce (2) | 2023.04.25 |
---|---|
[JavaScript] Map 메서드와 함수형 프로그래밍, 다형성 (2) | 2023.04.23 |
[JavaScript] Number, Math (0) | 2022.09.27 |
[JavaScript] Symbol (0) | 2022.09.27 |
[JavaScript] 객체 메소드(Object methods), 계산된 프로퍼티(Computed property) (0) | 2022.09.26 |