함수형 프로그래밍 시리즈 내용으로 계속 이어서 내용이 진행되므로 처음 부터 포스팅을 확인해주세요.
객체 역시 이터러블 프로그래밍을 통해 지연성, 동시성 등 이터러블 프로그래밍의 장점을 가져갈 수 있으며 그 방법에 대하여 알아보자.
const obj1 = {
a: 1,
b: 2,
c: 3,
d: 4
};
먼저 위코드의 values 값들을 콘솔을 통해 출력해보자.
console.log(Object.values(obj1)); // [1, 2, 3, 4]
위 처럼 value값들을 뽑아낼 수 있으며, 위 값들을 go함수를 `통해 값을 뽑아내는 함수를 보자.
go(
obj1,
Object.values,
map(a => a + 10),
take(2),
reduce((a, b) => a + b),
console.log); // 23
위 값에서 console.log를 하기까지 단 2개의 값이 필요했지만, 사실상 모든 values 값들을 모두 조회했을 것이다. 위처럼 값이 작은 데이터의 경우엔 상관 없지만, 데이터 양이 막대해지만 많은 손실이 일어날 것이다.
이를 좀 더 효과적으로 사용하기 위해서 L.values를 선언해보자.
L.values = function* (obj){
for(const k in obj){
yield obj[k];
}
}
그리고 이 값들을 전부 지연성 있게 출력하기위해 다음과 같이 설정 해주면 효과적인 이터러블 프로그래밍을 할 수 있다.
go(
obj1,
L.values,
L.map(a => a + 10),
take(2),
reduce((a, b) => a + b),
console.log); // 23
L.values에 콘솔을 하나하나 찍어보면 필요한 값만 사용됨을 알 수 있다.
L.entries = function* (obj){
for(const k in obj){
yield [k, obj[k]];
}
}
entries를 통해 나온 값들을 takeAll을 통해 모두 뽑아보면 다음과 같이 나온다.
go(
obj1,
L.entries,
takeAll,
console.log);
// ▼(4) [Array(2), Array(2), Array(2), Array(2)]
// ▶0: (2) ["a", 1]
// ▶1: (2) ["b", 2]
// ▶2: (2) ["c", 3]
// ▶3: (2) ["d", 4]
// length: 4
// ▶__proto__: Array(0)
이 값들을 다음과 같이 다룰 수 있다.
go(
obj1,
L.entries,
L.filter(([_, v]) => v % 2), // value가 홀수 인것만 뽑기 ["a", 1] ["c", 3]
L.map(([k, v]) => ({ [k]: v }), // 객체화 시키기 {a: 1} {c: 3}
reduce(Object.assign), // assign을 통해 합치기
console.log); // {a: 1, c: 3}
이처럼 L.value와 L.entries 함수를 사용하는 이유는 이터러블화 하는 함수를 중간에 둠으로써 이후에 객체의 이터러블 프로그래밍이 수월한 것을 보여주는 것이다.
keys 역시 이터러블로 다룰 수 있다.
L.keys = function* (obj){
for(const k in obj){
yield k;
}
}
이를 각각 뽑아보면 다음과 같이 각각의 key들을 추출할 수 있다.
go(
obj1,
L.keys,
each(console.log)); // a b c d
자바스크립트에서 제너레이터라는 강력한 도구를 활용하여 어떠한 값도 이터러블 프로그래밍을 통해 표현할 수 있다.
위에서 알아보았던 객체뿐 아니라 그 전 까지 다루었던 배열 등 정말 다양한 데이터들을 제너레이터로 이터러블화 하여 사용이 가능하므로 사용성 역시 명령형 프로그래밍에 비해 떨어지지 않는다.
[['a', 1], ['b', 2], ['c', 3]]
같은 entries 값을 {a: 1, b: 2, c: 3}
과 같은 객체로 만드는 함수로 object 함수를 구현하여 보자.
const a = [['a', 1], ['b', 2], ['c', 3]]; /* entries */
const object1 = pipe( /*entries를 받음*/
map(([k, v]) => ({ [k]: v })),
reduce(Object.assign));
console.log(object1(a)); // {a: 1, b: 2, c: 3}
위 object는 워낙 간단한 자료구조여서 reduce하나만을 사용해서 나타내어도 된다.
const object = entries => reduce((obj, [k, v]) => (obj[k] = v, obj), {}, entries);
이것을 사용했을때 어떤점이 개선되는지 보자.
다음과 같은 Map값이 있다.
let m = new Map();
m.set('a', 10);
m.set('b', 20);
m.set('c', 30);
console.log(m); // Map(3) {"a" => 10, "b" => 20, "c" => 30}
이 값을 JSON 외부 서버로 값을 보낼때 object화 하지않으면 보낼 수 없다.
JSON.stringify({a:1, b:2}) // "{"a":1,"b":2}"
JSON.stringify(m) // "{}"
그럼 이 map값을 object로 어떻게 만들 수 있을까? 바로 위에서 작성했던 object를 통해 통과시키면된다.
JSON.stringify(object(m)) // "{"a":10,"b":20,"c":30}"
이것이 가능한 이유는 무엇일까? Map값 역시 이터러블을 지원하기 때문이다.
즉, entries를 발생시킬수 표준이 맞춰진 이터러블 문법을 자바스크립트는 개발자에게 제공하고 이러한 이터러블은 전부 object로 인해 객체 형태로 바꿀 수 있다.
mapObject 함수는 객체를 받아, map을 통해 값을 적용시키고 배열을 다시 원래의 객체형태로 반환해주는 것이다.
다음과 같은 동작을 한다.
const mapObject = _ => _;
console.log(mapObject(a => a + 10, { a: 1, b: 2, c: 3}));
// { a: 11, b: 12, c: 13 }
위와 같은 동작을 하기 위해선 먼저 위 객체 값을 entries로 만들어야 할 것이며, 거기에다가 map을 적용하고 다시 객체화 시켜야 할 것이다.
아래와 같이 동작하는 사고방식을 미리 구상하면 편하게 짤 수 있다.
[['a', 1], ['b', 2], ['c', 3]] /* 1. entries화 시키기*/
[['a', 11], ['b', 12], ['c', 13]] /* 2. map 적용*/
{ a: 11, b: 12, c: 13 } /* 3. object 함수 사용 */
이제 이 것을 코드로 정리해보면 다음과 같이 나온다.
const mapObject = (f, obj) => go(
obj,
L.entries,
L.map(([k, v]) => [k, f(v)]),
object);
console.log(mapObject(a => a + 10, { a: 1, b: 2, c: 3}));
// {a: 11, b: 12, c: 13}
pick 함수는 객체가 있을 때 해당하는 키 값들에 대한 객체만을 뽑아 새로운 객체를 반환하는 함수이다.
다음과 같은 동작을 한다.
const obj2 = { a: 1, b: 2, c: 3, d: 4, e: 5 };
const pick = _ => _;
console.log(pick(['b', 'c'], obj2));
// { b: 2, c: 3 }
이 역시 짜기전에 미리 구상하면 좋은데, pick있는 값들을 keys로 받아서, 그냥 object화 시키면 된다.
const pick = (ks, obj) => _.go(
ks,
map(k => [k, obj[k]]),
object);
console.log(pick(['b', 'c'], obj2));
// { b: 2, c: 3 }
만약 없는 값을 pick으로 넣었을 경우 어떻게 되는지 보자.
console.log(pick(['b', 'c', 'z'], obj2)); // {b: 2, c: 3, z: undefined}
z 값은 undefined로 나왔는데, 이는 매우 안좋은 데이터이다. 왜냐하면 데이터 통신 간에(ex: JSON 데이터) undefined 값은 보낼 수 없기 때문이다.
JSON.stringify({b: 2, c: 3, z: undefined})
// "{"b":2,"c":3}"
그럼 이 undefined 값을 따로 처리해주는 구문을 추가해줘야한다. 마무리로 이 pick 함수를 지연성있게 정리해주자.
const obj2 = { a: 1, b: 2, c: 3, d: 4, e: 5 };
const pick = (ks, obj) => go(
ks,
L.map(k => [k, obj[k]]),
L.filter(([k, v]) => v !== undefined),
object);
console.log(pick(['b', 'c', 'z'], obj2));
// { b: 2, c: 3 }
indexBy라는 함수는 값을 key, value 쌍으로 만들어서 데이터를 조회하는 비용을 줄여주는 것이다. 이해하기에는 코드가 더 직관적이므로 다음을 살펴보자.
다음과 같은 users 데이터가 있다.
const users = [
{ id: 5, name: 'AA', age: 35 },
{ id: 10, name: 'BB', age: 26 },
{ id: 19, name: 'CC', age: 28 },
{ id: 23, name: 'DD', age: 34 },
{ id: 24, name: 'EE', age: 23 }
];
이 데이터를 조회할때 그냥 user를 조회하면 단순 배열로 나온다.
console.log(users);
...
▼(5) [{…}, {…}, {…}, {…}, {…}]
▶0: {id: 5, name: "AA", age: 35}
▶1: {id: 10, name: "BB", age: 26}
▶2: {id: 19, name: "CC", age: 28}
▶3: {id: 23, name: "DD", age: 34}
▶4: {id: 24, name: "EE", age: 23}
length: 5
▶__proto__: Array(0)
...
하지만, indexBy를 통해 다음과 같이하면 id 값이 index가 될 것이다.
console.log(indexBy(u => u.id, users));
...
▼{5: {…}, 10: {…}, 19: {…}, 23: {…}, 24: {…}}
▶5: {id: 5, name: "AA", age: 35}
▶10: {id: 10, name: "BB", age: 26}
▶19: {id: 19, name: "CC", age: 28}
▶23: {id: 23, name: "DD", age: 34}
▶24: {id: 24, name: "EE", age: 23}
▶__proto__: Object
...
이렇게 바꾸면 어떤 이점이 있을까? 그냥 일반적인 users에서 값을 찾을때는 해당 값을 찾기 전까지 계속해서 순회하다가 값을 찾을 것이다.
find(u => u.id == 19, users); // {id: 19, name: "CC", age: 28}
/* id가 19인 것을 찾기 위해 그전 데이터를 모두 조회함 */
그러나, index가 id값이 되어 있으면 단순히 그 해당 값의 키를 통해 조회할 수 있다.
const users2 = indexBy(u => u.id, users);
...
users2[19]
// {id: 19, name: "CC", age: 28}
users2[23]
// {id: 23, name: "DD", age: 34}
...
이러한 indexBy는 다음과 같이 구현할 수 있다.
const indexBy = (f, iter) => reduce((obj, a) => (obj[f(a)] = a, obj), {}, iter);
const users2 = indexBy(u => u.id, users);
console.log(users2);
...
▼{5: {…}, 10: {…}, 19: {…}, 23: {…}, 24: {…}}
▶5: {id: 5, name: "AA", age: 35}
▶10: {id: 10, name: "BB", age: 26}
▶19: {id: 19, name: "CC", age: 28}
▶23: {id: 23, name: "DD", age: 34}
▶24: {id: 24, name: "EE", age: 23}
▶__proto__: Object
...
이제 이러한 indexBy 된 값에 조건을 추가해 filter를 해보자. 이 역시도 그냥 filter하려고 하면 되지않아서, entries화 시킨 후에 filter를 하고, 다시 object해주면 된다.
go(
users2,
L.entries,
L.filter(([_, {age}]) => age < 30),
object,
console.log);
...
▼{10: {…}, 19: {…}, 24: {…}}
▶10: {id: 10, name: "BB", age: 26}
▶19: {id: 19, name: "CC", age: 28}
▶24: {id: 24, name: "EE", age: 23}
▶__proto__: Object
...
또 이 값을 users3에 넣어서 id 값을 찾는 경우에도 간단하게 할 수 있다.
const users3 = go(
users2,
L.entries,
L.filter(([_, {age}]) => age < 30),
object);
console.log(users3[19]);
// {id: 19, name: "CC", age: 28}