함수형 프로그래밍 - 객체를 이터러블 프로그래밍으로 다루기

MyeonghoonNam·2021년 8월 15일
1

함수형 프로그래밍

목록 보기
9/10

함수형 프로그래밍 시리즈 내용으로 계속 이어서 내용이 진행되므로 처음 부터 포스팅을 확인해주세요.


객체 역시 이터러블 프로그래밍을 통해 지연성, 동시성 등 이터러블 프로그래밍의 장점을 가져갈 수 있으며 그 방법에 대하여 알아보자.

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),
  take(2),
  reduce((a, b) => a + b),
  console.log); // 23

L.values에 콘솔을 하나하나 찍어보면 필요한 값만 사용됨을 알 수 있다.


entries

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

어떠한 값도 다룰 수 있는 이터러블 프로그래밍

자바스크립트에서 제너레이터라는 강력한 도구를 활용하여 어떠한 값도 이터러블 프로그래밍을 통해 표현할 수 있다.

위에서 알아보았던 객체뿐 아니라 그 전 까지 다루었던 배열 등 정말 다양한 데이터들을 제너레이터로 이터러블화 하여 사용이 가능하므로 사용성 역시 명령형 프로그래밍에 비해 떨어지지 않는다.


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

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

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

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}
profile
꾸준히 성장하는 개발자를 목표로 합니다.

0개의 댓글