#1 객체를 다루는 함수, 이터러블 프로그래밍화 시키기
values 다루기
다음과 같은 객체가 있다고 보자.
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),
L.take(2),
_.reduce((a, b) => a + b),
console.log); // 23
L.values에 콘솔을 하나하나 찍어보면 필요한 값만 사용됨을 알 수 있다.
L.values = function* (obj){
for(const k in obj){
console.log(k);
yield obj[k];
}
}
_.go(
obj1,
L.values,
L.map(a => a + 10),
L.take(2),
_.reduce((a, b) => a + b),
console.log); // a b 23
entries 다루기
이 역시도 L.value와 코드는 유사하다.
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 다루기
keys역시 유사하다.
L.keys = function* (obj){
for(const k in obj){
yield k;
}
}
이를 각각 뽑아보면 다음과 같이 key들이 뽑혀나온다.
_.go(
obj1,
L.keys,
_.each(console.log)); // a b c d
#2 어떠한 값이든 이터러블 프로그래밍으로 다루기
object 다루기
Object는 [['a', 1], ['b', 2], ['c', 3]] 같은 entries값을 {a: 1, b: 2, c: 3} 과 같은 객체로 만드는 함수이다.
이러한 동작을 하는 object함수를 작성해보자.
const a = [['a', 1], ['b', 2], ['c', 3]]; /* entries */
const object = _.pipe( /*entries를 받음*/
L.map(([k, v]) => ({ [k]: v })),
_.reduce(Object.assign));
console.log(object(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
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로 나왔는데, 이는 매우 안좋은 데이터이다. 왜냐하면 JSON 데이터로 보낼때 undefined 값은 보낼 수 없기 때문이다.
JSON.stringify({b: 2, c: 3, z: undefined})
// "{"b":2,"c":3}"
그럼 이 undefined 값을 따로 처리해주는 구문을 추가해줘야한다.
const obj2 = { a: 1, b: 2, c: 3, d: 4, e: 5 };
const pick = (ks, obj) => _.go(
ks,
_.map(k => [k, obj[k]]),
_.reject(([k, v]) => v === undefined),
object);
console.log(pick(['b', 'c', 'z'], obj2));
// { b: 2, c: 3 }
마무리로 이 pick 함수를 지연성있게 정리해주자.
const pick = (ks, obj) => _.go(
ks,
L.map(k => [k, obj[k]]),
L.reject(([k, v]) => v === undefined),
object);
indexBy
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는 다음과 같이 코드를 짤 수 있다.
_.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}
'Web[웹] > ES6+ 함수형 언어' 카테고리의 다른 글
[ES6+: 응용] 시간을 이터러블로 다루기 (0) | 2019.11.01 |
---|---|
[ES6+: 응용] 사용자 정의 객체를 이터러블 프로그래밍으로 다루기 (0) | 2019.11.01 |
[ES6+: 응용] map과 filter로 하는 안전한 함수 합성 (0) | 2019.10.29 |
[ES6+: 응용] reduce함수를 통해보는 명령형 습관을 지우기 (0) | 2019.10.25 |
[ES6+: 응용] 명령형에서 함수형으로 변환시키기 (0) | 2019.10.24 |