- Published on
ES6) Iteration을 우아하게 하는 법
- Authors
- Name
- Hoehyeon Jung
iteration
ES6 이후부터는 iteration에 대한 개념이 적립됨에 따라, 순회를 좀 더 효율적으로 수행할 수 있게 되었다.
iterator와 iterable
iterator 프로토콜을 구현하기 위해서는 다음과 같은 조건이 필요하다.
- iterator.next를 가진다.
value: ..., done: Boolean
의 구조(interface)를 만족해야 한다.
const makeIterator = function(array) {
let nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{ value: array[nextIndex++], done: false } :
{ value: undefined, done: true }
}
}
}
const hi = makeIterator([1, 2, 3, 4]);
console.log(hi.next()); // {value: 4, done: false}
console.log(hi.next()); // {value: 3, done: false}
console.log(hi.next()); // {value: 2, done: false}
console.log(hi.next()); // {value: 1, done: false}
console.log(hi.next()); // {value: undefined, done: true}
iterable 프로토콜을 구현하기 위해서는 다음과 같은 조건이 필요하다.
Symbol.iterator
를 가지고 있으며 그 값이 iterator여야 한다.
const makeIterable = function (array) {
let nextIndex = 0;
return {
[Symbol.iterator]() { return this },
next: function() {
return nextIndex < array.length ?
{ value: array[nextIndex++], done: false } :
{ value: undefined, done: true }
}
}
}
const hello = makeIterable([1, 2, 3, 4]);
for(let data of hello) { console.log(data); }
iterable 프로토콜의 특징
iterable 프로토콜을 구현하기 위해서는 위와 같은 조건을 만족해야 한다. 하지만 이러한 프로토콜을 구현하는 번거로움이 있는 데도 구현할 이유가 있을까? 물론 그만한 이득을 가지고 있다. 다음의 특징들을 살펴보며, iterable 프로토콜이 갖는 특징들을 파악해보자.
다양한 ES6 연산자 활용 가능
iterable 프로토콜을 만족할 경우, for .. of
, spread
, rest
연산자를 적용하여 우아하게 순회 및 분해 할당이 가능해진다.
const reverseIterable = (list) => {
let cur = list.length;
return {
[Symbol.iterator]() {
return {
next: () => cur-- > 0 ?
{ value: list[cur], done: false } :
{ value: undefined, done: true }
}
},
}
}
const originArr = [1, 2, 3, {val: 4}];
const arr = reverseIterable(originArr);
const newArr = [...arr];
const [x, y, ...xs] = reverseIterable(originArr);
console.log(newArr, x, y, xs); // [4, 3, 2, 1] 4 3 [2, 1]
x.val = 5; // originArr is mutated because destructing assignment is SHALLOW COPY!
console.log(originArr, newArr); // [1, 2, 3, {val: 5}] [{val: 5}, 3, 2, 1]
Spread 연산자는 iterable 프로토콜을 만족하는 Collection들을 배열로 나열시키는 역할을 한다. 구조 분해 할당(Destructing assignment), Rest parameter 역시 iterable 프로토콜을 만족하면 가능한 연산이다. 하지만 이러한 연산자들은 참조된 값인 경우 참조를 복사하기 때문에(Shallow Copy) 원본이 복사될 가능성도 있어 주의해야 한다.
iterable로 구현된 자료형
Array(TypedArray), String 등 같은 경우 내부적으로 iterable이 구현되어 있다.
console.log(String.prototype[Symbol.iterator]); // ƒ [Symbol.iterator]() { [native code] }
console.log(Array.prototype[Symbol.iterator]); // ƒ values() { [native code] }
const iterable1 = [1, 2, 3, 4];
const iterator1 = iterable1[Symbol.iterator]();
console.log(iterator1.next()); // { value: 1, done: false }
console.log(iterator1.next()); // { value: 2, done: false }
console.log(iterator1.next()); // { value: 3, done: false }
console.log(iterator1.next()); // { value: 4, done: false }
console.log(iterator1.next()); // { value: undefined, done: true }
Set, Map 역시 iterable 프로토콜의 데이터를 받기도 하고, 각각 iterable 프로토콜을 지키는 자료형으로 반환한다.
const arr = [1, 2, 3, 4, 3];
// Set, Map can get Iterable data
const mySet = new Set([...arr]);
const userMap = new Map([['id', 'gonewbie'], ['language', 'javascript']]);
console.log([...mySet]); // [1, 2, 3, 4]
for (const prop of userMap) {
console.log(prop); // ['id', 'gonewbie'], ['language', 'javascript']
}
또한, 브라우저 영역에서 기존의 유사 배열(array-like)로 알려져 있던 NodeList, arguments 등도 iterable 프로토콜을 따르도록 구현하는 추세이므로 더 적극적으로 iterable을 이용한 순회가 가능해졌다.
const nodeLists = [...document.querySelectorAll('p')];
for (const elem of nodeLists) {
console.log(elem);
}
iterable in Functional Programming
ES6 이후로 iterable 프로토콜을 지키는 자료형이 늘어남에 따라 이러한 iterable 프로토콜을 함수형으로 다루면 어떨까? 하는 마음도 생기기 마련이다. 다양한 ES6 연산자를 통해 iterable 프로토콜을 선언적으로 다룰 수 있다.
iterate Array ES6
자바스크립트에서의 배열은 순서를 가진 데이터를 처리하는 자료형으로 ES6 이후에는 다양한 고차함수를 통해 데이터를 좀 더 우아하게 처리할 수 있게 되었다. ES5까지는 다음과 같은 방식을 통해 배열을 순회했다.
var arr = [1, 2, 3, 4, 5];
// this is anti-pattern!! DON'T use like this!!
for (var index in arr) {
console.log(index);
console.log(arr[index]);
}
// .forEach() loop mutate each element
arr.forEach((elem, index) => {
console.log(elem * 2);
return arr[index] = elem * 2;
});
console.log(arr);
for .. in
loop를 배열에서 이용하는 것은 매우 바람직하지 않다. 배열 내 index만 순회하는 것뿐 아니라 Array의 prototype chain도 순회하기 때문에 사용하지 말아야 한다. ES5에서 추가된 .forEach()
메서드는 이러한 loop를 잘 추상화하였다. 하지만 forEach
에서는 사용을 주의하여야 할 사항들이 있다. 바로 원본 배열이 바뀔 수 있다는 것이다. 배열 값이 mutable한 것은 해당 배열을 사용하는 함수들 모두에게 side effect가 전파될 수 있음을 의미한다.
하지만, ES6부터는 이러한 걱정을 덜어낼 수 있게 되었다. Array.prototype.map
, Array.prototype.filter
, Array.prototype.reduce
등 순회가 포함된 고차함수를 활용하여 처리가 가능해졌기 때문이다. 물론 iterable 프로토콜을 통해 문제를 해결하는 방법도 있다.
const pipe = (...functions) =>
initValue => functions.reduce((acc, func) => func(acc), initValue);
const uniqueDessert = x => [...new Map(x)];
const changeForm = obj => Object.values(obj).map(x => [x.id, x.name]);
const names = x => x.map(elem => elem[1]);
const data = [{id: 1, name: 'chocolate'}, {id: 2, name: 'cookie'}, {id: 1, name: 'candy'}];
const getUniqueDessert = pipe(
changeForm,
uniqueDessert,
names
);
const desserts = getUniqueDessert(data);
console.log(desserts); // ['candy', 'cookie']
Map/Set과 같은 경우, unique한 데이터의 입력을 받기 때문에 iterable로 입력된 데이터의 중복을 방지할 수 있는 좋은 방법이다. 이러한 특성을 통해 데이터를 정제하고 Map/Set과 같은 데이터는 JSON으로 직렬화되지 않기 때문에 배열 등으로 변경하여 서버와 통신을 하면 된다.
마치며
자바스크립트에서 iterator/iterable 프로토콜은 loop를 좀 더 선언적으로 만들어주는 강력한 툴이고, 이를 지원할 강력한 연산자들 역시 제공하고 있다. 이러한 연산자를 적절하게 사용해서 문제를 해결하는 능력을 기를 수 있는 개발자가 되도록 노력해야겠다.
참조