본문 바로가기

Web[웹]/ES6+ 함수형 언어

[ES6+: 응용] 객체를 이터러블 프로그래밍으로 다루기

 

#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}