본문 바로가기

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

[ES6+: 응용] reduce함수를 통해보는 명령형 습관을 지우기

 

#1 reduce함수는 만능이 아니다

 

"reduce" 한개를 쓰는 것 보다 "map + filter + reduce" 를 함께 쓰자

 

기존 명령형 습관들 때문에, reduce를 오용하는 경우가 자주 있다.

 

const users = [
    { name: 'AA', age: 35},
    { name: 'BB', age: 26},
    { name: 'CC', age: 28},
    { name: 'DD', age: 34},
    { name: 'EE', age: 23},
]

 

위와 같은 users 데이터가 있다. 이를 합산하는 코드를 reduce통해 짜볼 것이다.

 

console.log(_.reduce((total, u) => total + u.age, 0, users)); // 146

 

위를 보면 total과 u.age라는 서로 다른 형태를 통해 합산을 하는 것을 알 수 있는데, reduce에서 서로 합산 할 때는 서로 형이 같아야지 더 간단하며 좋은 코드라고 할 수 있다.

 

다시 말해서, 위 코드 처럼 하나의 보조함수에서 복잡하게 처리하는 것 보다, reduce에 집어넣기 전에 데이터를 통일 시켜서 연산하는 것이 더 좋은 코드라는 말이다.

 

console.log(_.reduce((a, b) => a + b, L.map(u => u.age, users))); // 146

 

위와같이 형이 같은 인자가 두개 들어오는 함수의 경우에는 다음처럼 심플하게 코드를 짤 수 있다.

 

const add = (a, b) => a + b;
const ages = L.map(u => u.age);

console.log(_.reduce(add, ages(users))); // 146

 

비슷한 예제로 하나 더 보자.

만약, 나이가 30살 미만인 유저의 나이만을 더한다고 했을 때, reduce로만 코드를 짠다면 다음과 같이 나올 것이다.

 

/* if문 이용 */
console.log(
    _.reduce((total, u) => {
        if(u.age >= 30) return total;
        return total + u.age;
    },
    0,
    users)); // 77
    
-----------------------------------------------------
/* 삼항 연산자 이용 */
console.log(_.reduce((total, u) => u.age >= 30 ? total : total + u.age, 0, users)); // 77

 

위와 같이 표현이 가능하지만, map과 filter를 같이 쓰면서, 복잡성을 줄일 수 있다.

 

console.log(
   _.reduce(add,
       _.filter(u => u < 30,
           _.map(u => u.age, users))));
           
------------------------------------------
/* 지연적으로 동작하게 하기 */
console.log(
   _.reduce(add,
       L.filter(u => u < 30,
           L.map(u => u.age, users))));

 

그래서 코드를 만들어 갈때, reduce하나를 사용해서 복잡하게 사용하는 것보다, reduce와 map이나 filter를 같이 사용해서 하나의 형태를 사용하는 인자로 만들어서 사용하는 것이 훨씬 유리하고 이터러블 프로그래밍을 더 잘하게 되는 방법이 될 것 이다.

 

 

 


 

 

query1, query2, query3, query4 코드 비교하기

 

아래 obj1의 출력을 'a=1&c=CC&d=DD'가 대도록 만들어 볼 것이다.

먼저, 각 query 함수들은 다음의 목적을 가지고 구성을 할 것 이다.

 

  1. query1: 명령형으로만 구성
  2. query2: reduce함수만 구성
  3. query3: "reduce + 다른 함수" 를 통해 구성
  4. query4: go, pipe, curry 함수를 통해 간단히 구성

 

const obj1 = {
    a: 1,
    b: undefined,
    c: 'CC',
    d: 'DD'
};

/* 실행결과가 'a=1&c=CC&d=DD'가 대도록 만들기 */

 

query1은 명령형으로 코드를 짜볼 것이다.

 

function query1(obj){
   let res = '';
   for(const k in obj){
      const v = obj[k];
      if(v === undefined) continue;
      if(res != '') res += '&';
         res += k + '=' + v;
   }
   return res;
}

console.log(query1(obj1)); //a=1&c=CC&d=DD

 

쭉 조건을 일치하게 만들어서 나온 코드는 생각보다 복잡한 것을 볼 수 있다.

 

이를 reduce 하나만을 이용한 함수형으로 짠 query2를 보자.

 

function query2(obj){
    return Object
    .entries(obj) // key, value 쌍으로 순회할 수 있게끔 뽑힘
    .reduce((query, [k, v], i) => {
        if(v === undefined) return query;
        return `${query}${i > 0 ? '&' : ''}${k}=${v}`;
    }, '');
}

console.log(query2(obj1)); // a=1&c=CC&d=DD

 

여전히 복잡해 보인다. 그래서 앞에서 배웠던 reduce에 각종 함수를 혼합해서 사용해보자.

 

const join = (sep, iter) =>
   _.reduce((a, b) => `${a}${sep}${b}`, iter);

const query3 = obj =>
   join('&',
      _.map(([k, v]) => `${k}=${v}`,
         _.reject(([_, v]) => v === undefined, //받긴하나 안쓰는 변수는 '_' 로 나타내기
            Object.entries(obj))));

console.log(query3(obj1)); // a=1&c=CC&d=DD

 

마지막으로 go 함수를 통해 함수를 훨씬 읽기 간결하게 정리하는 함수 query4를 보자.

 

const join = _.curry((sep, iter) =>
   _.reduce((a, b) => `${a}${sep}${b}`, iter));

const query4 = obj => _.go(
   obj,
   Object.entries,
   _.reject(([_, v]) => v === undefined),
   _.map(join('=')),
   join('&')
);

console.log(query4(obj1)); // a=1&c=CC&d=DD

 

join에 curry를 넣고 go를 이용해서 읽기 간결하게 만들었다. 이 코드는 obj인자를 obj를 그대로 받기때문에 pipe로도 대체 가능하다.

 

const query4 = _.pipe(
    Object.entries,
    _.reject(([_, v]) => v === undefined),
    _.map(join('=')),
    join('&'));

console.log(query4(obj1)); // a=1&c=CC&d=DD

 

query1에서 query4로 갈수록 더 함수형 프로그래밍적으로 이터러블 가능한 프로그래밍이 가능한데, 훨씬 코드도 간결해지고 읽기도 편해진다.

 

 

반대로, 위에서 했었던 query 스트링을 Object로 만드는 코드를 작성해볼 것이다.

 

▼가장 먼저 split라는 함수로 '&' 기준으로 스트링을 분리해준다.

 

const split = _.curry((sep, str) => str.split(sep));

const queryToObject = _.pipe(
    split('&')
);

console.log(queryToObject('a=1&c=CC&d=DD'));
// ["a=1", "c=CC", "d=DD"]

 

▼이어서, map을 통해 더 세밀하게 분리해준다.

 

const split = _.curry((sep, str) => str.split(sep));

const queryToObject = _.pipe(
    split('&'),
    _.map(split('='))
);

console.log(queryToObject('a=1&c=CC&d=DD'));
// ▼(3) [Array(2), Array(2), Array(2)]
//  ▶0: (2) ["a", "1"]
//  ▶1: (2) ["c", "CC"]
//  ▶2: (2) ["d", "DD"]
//    length: 3
//  ▶__proto__: Array(0)

 

▼이후 또 map을 통해 key와 value 쌍으로 합쳐준다.

 

const split = _.curry((sep, str) => str.split(sep));

const queryToObject = _.pipe(
    split('&'),
    _.map(split('=')),
    _.map(([k, v]) => ({ [k]: v}))
);

console.log(queryToObject('a=1&c=CC&d=DD'));
// ▼(3) [{…}, {…}, {…}]
//  ▶0: {a: "1"}
//  ▶1: {c: "CC"}
//  ▶2: {d: "DD"}
//    length: 3
//  ▶__proto__: Array(0)

 

▼마지막으로 reduce를 통해 assign 해주면 다음과 같이 합쳐진다.

 

const split = _.curry((sep, str) => str.split(sep));

const queryToObject = _.pipe(
    split('&'),
    _.map(split('=')),
    _.map(([k, v]) => ({ [k]: v})),
    _.reduce(Object.assign)
);

console.log(queryToObject('a=1&c=CC&d=DD'));
// {a: "1", c: "CC", d: "DD"}