이 글은 유인동님의 "함수형 프로그래밍과 JavaScript ES6+" 강의를 수강하면서 작성한 포스팅입니다.
이번 포스팅에서 다룰 주제들입니다.
- 리스트 순회
- 이터러블 및 이터레이터 프로토콜
const list = [1, 2, 3];
for (var i = 0; i < list.length; i++) {
log(list[i]);
}
const str = 'abc';
for (var i = 0; i < str.length; i++) {
log(str[i]);
}
이전 ES5에서는 이런식으로 for문을 사용했었다.
지금의 나도 이런 형태로 프로그래밍을 하고 있었다. (생각날때는 for-of문)
하지만 ES6가 도입되면서 순회하는 방식이 달라졌다.
for (const a of list) log(a);
for (const a of str) log(a);
ES5보다 문법적으로 훨씬 간편해진 것을 볼 수 있다.
하지만 알아야 할 것은 단순히 간결하게 하기 위한 변화가 아닌 내장되어 있는 의미가 있다는 것이다. 지금부터 내장되어 있는 의미가 무엇인지 알아보려고 한다!
크게 3가지 파트로 나눠서 정리해보도록 하겠다.
위에서 for-of가 문법적으로만 간단해진 것이 아니라 내장된 의미가 있다고 설명했었다.
그에 대한 근거가 무엇인지 한번 확인해보자.
const arr = [1, 2, 3];
log(arr[1]); // 2
log(arr[2]); // 3
const set = new Set([1, 2, 3]);
log(set[0]); // undefined
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
log(map[0]); // undefined
Array 같은 경우에는 index(key)를 통해서 해당 배열의 원소에 접근할 수 있다 (arr[0], arr[1] 등)
하지만 Set과 Map의 경우에는 Array 같이 key를 통해서 조회할 수 없다 (undefined)
이렇게 값이 조회가 되지 않는다는 것이 for-of가 위에서 써놓은 것처럼 ES5에서 사용했던 일반적인 반복문 형식과 전혀 다르다는 것을 의미한다.
지금부터 for-of문이 어떻게 동작을 하고 있는지 알아보도록 하자!
설명에 앞서 자바스크립트의 Symbol 개념을 간단하게라도 알고 있어야 이해가 가능하다.
괜찮은 자료 링크를 찾았으니 참고로 하면 좋을 것 같다.
추가로 미리 알아놔야 할 포인트들을 간단하게 정리하겠다.
- Symbol은 객체의 key로도 사용될 수 있다.
- Symbol.iterator라는 미리 정의되어 있는 심볼이 있다. 이는 이터러블한 객체를 정의하기 위한 심볼이다.
const arr = [1, 2, 3];
log(arr[Symbol.iterator]); // [Function: values]
arr[Symbol.iterator] = null;
for (const a of arr) log(a); // error: arr is not iterable
해당 배열의 Symbol.iterator 키에 대응하는 value가 무엇인지 확인을 해보면 함수가 하나 들어있다.
여기서 Symbol.iterator 키에 대응하는 value 함수를 null처리하고 반복문을 돌려보면 arr is not iterable
이라는 에러가 발생하게 된다. 이는 Set과 Map도 동일하다.
즉, for-of와 Symbol.iterator가 연관이 있다는 의미이다!
이 시점에서 이터러블과 이터레이터는 무엇이고 이터러블/이터레이터 프로토콜은 무엇인지 개념을 한번 잡아보자.
이터러블: 이터레이터를 리턴하는 Symbol.iterator를 가진 값
이터레이터: { value, done } 객체를 리턴하는 next()를 가진 값
이터러블/이터레이터 프로토콜: 이터러블을 for...of, 전개 연산자 등과 함께 동작하도록 한 규약
Array, Set, Map은 자바스크립트 내장 객체이고 이터러블/이터레이터 프로토콜 규약을 따르고 있다.
위에 정의가 잘 이해가 안될수도 있다. (나도 이해하는데 진짜 과장 조금 보태서 3시간정도 걸린것 같다)
이해가 안되는 사람들을 위해 최대한 잘 정리를 해보자면..!
const arr = [1, 2, 3];
let iterator = arr[Symbol.iterator](); // Object [Array Iterator] {}
log(iterator.next()); // { value: 1, done: false }
log(iterator.next()); // { value: 2, done: false }
log(iterator.next()); // { value: 3, done: false }
log(iterator.next()); // { value: undefined, done: true }
배열(arr)은 기본적으로 이터러블이다. 아까 위에서 arr[Symbol.iterator]를 찍어보니 함수(메서드)가 나왔었다. 이것이 이터러블이라는 개념이다. arr은 이터레이터를 리턴하는 Symbol.iterator 메서드를 가지고 있는 값이기 때문이다.
이터레이터는 { value, done }을 리턴하는 next()를 가진 값이라고 정리해놓았다. 위의 코드에서 iterator라고 변수를 지정하게 되면 iterator 변수는 value와 done이라는 key를 가진 객체가 된다. 이 객체는 next 메서드를 가지는데 next 메서드를 통해 해당 배열의 원소들에 접근할 수 있게 된다. 여기서 value가 for-of문에서 우리가 설정하는 변수에 들어가게 되는 것이다.
이터러블/이터레이터 프로토콜은 쉽게 말해서 Array,Set,Map(이터러블)이 for-of문, 전개 연산자와 함께 동작할 수 있도록 규약된 프로토콜이라고 생각하면 된다.
Array가 이터러블이고 Symbol.iterator를 통해서 이터레이터를 리턴하기 때문에 for-of문과 함께 동작하는 이터러블 객체이고 순회도 가능하기 때문에 이터러블/이터레이터 프로토콜을 따른다.
추가로 몇가지 조금 더 살펴보자면 Map에는 keys()라는 메서드가 있다. 이 메서드는 iterator를 리턴한다. (values와 entries라는 메서드도 동일하다)
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
const a = map.keys(); // MapIterator {"a", "b", "c"}
log(a.next()); // a
log(a.next()); // b
log(a.next()); // c
const it = map.values(); // 여기서 iterator 리턴
const it2 = it[Symbol.iterator](); // 여기서 다시 Symbol.iterator를 확인해보면 iterator로 만든 것이 또 이터레이터를 가지고 있다.
it2.next(); // { value: 1, done: false }
it2.next(); // { value: 2, done: false }
it2.next(); // { value: 3, done: false }
it라는 변수에 해당 iterator를 저장했다. 이 변수에서 다시 Symbol.iterator를 확인해보면 이터레이터를 가지고 있다. 그 iterator의 next 메서드를 통해 for-of가 동작하는 것이다. 이것이 이터러블/이터레이터 프로토콜 동작 방식이다.
for (const a of map.values()) log(a);
해당 코드 동작방식은 스스로 해석해보자!
위의 코드만 한번 봐도 충분히 해석이 가능할 것 같다 :)
지금까지는 자바스크립트 내장값인 Array, Map, Set이 for-of로 어떻게 순회되는지를 확인해봤다.
이번에는 사용자 정의 이터러블을 구현하면서 이터러블에 대해서 더 자세하게 알아보려고 한다.
const iterable = {
[Symbol.iterator]() {
let i = 3;
return {
next() {
return i === 0 ? { value: undefined, done: true } : { value: i--, done: false };
},
[Symbol.iterator]() { // (A)
return this; // 자기자신을 return 해준다.
}
}
}
};
let iterator = iterable[Symbol.iterator]();
for(const a of iterable) log(a);
위의 코드에서 만약 A 부분이 없었다면 완벽한 커스텀 이터러블 객체가 아니었을 것이다.
저 부분이 완성이 되어야 상단의 코드가 성립이 될 수 있다.
잘만든 이터러블이란? (Well-formed iterable)
-> 이터레이터를 만들어서 순회를 할 때 잘 동작한다.
-> 일부 진행한 이후에는 진행된 결과 이후의 값들로 진행이 되야한다.
참고로 이터레이터 역시 Symbol.iterator를 가지고 있고 이 Symbol.iterator를 실행한 값은 바로 자기 자신이다. 하단의 코드가 바로 well-formed iterable이다.
const arr2 = [1, 2, 3];
let iter = arr2[Symbol.iterator]();
log(iter[Symbol.iterator]() == iter2) // true
전개연산자도 마찬가지로 이터러블/이터레이터 프로토콜 규약을 따르고 있다.
따로 다른 설명은 안하려고 한다. 위에 설명을 보면 충분히 이해할 수 있다.
const a = [1, 2];
// a[Symbol.iterator] = null; // error 발생
log([...a, ...[3, 4]]); // [1, 2, 3, 4]
이번 주제는 생각보다 너무너무 어려웠다. 지금 내가 쓴 글과 강의를 3번씩은 본 것 같은데 아직 이해가 절반밖에 되지 않은 것 같다. 운동 다녀와서 사이드 프로젝트 조금 더 다듬고.. 자기 전에 이해해보고 자야지 :(
https://catsbi.oopy.io/79e5ee3a-7c93-4ed6-a48f-2da6eab42109
https://hyoveemo.tistory.com/11