const obj1 = {
a: 1,
b: undefined,
c: 'CC',
d: 'DD'
}
이런 코드가 있다고 가정하고, 이를 'a=1, c=CC, d=DD'의 형태로 변환
하는 함수를 만들어본다고 생각하면 처음에는 쉽게 접근이 힘들 수 있다. 이를 먼저 명령형으로 작성해보면
function query1(obj) {
let res = "";
for (const k in obj) {
const v = obj[k];
if (v === undefined) continue; // value가 undefined면 지나친다.
if (res !== "") res += ", "; // obj를 순회 할 때마다 ", "를 더해주는데, 첫 번째 k에서는 제외하여 ", "로 시작하는 것을 방지한다.
res += k + "=" + v;
}
return res;
}
console.log(query1(obj1)); // a=1, c=CC, d=DD
위와 같이 작성해볼 수 있다. 다만 가독성이 떨어지고 코드가 길어진다는 단점이 있다. 차후 대규모 프로젝트에서는 명령형으로 작성된 코드가 함수형, 선언형 코드보다 수정이나 유지보수가 상대적으로 어려울 가능성이 높다.
✅
value === undefined
일 때는 순회에서 그냥 지나쳤는데, 이는 undefined라는 값이 JSON에 없기 때문에 서버에 전달하고 싶어도 전달할 방법이 없고, Database에 저장도 되지 않기 때문에 구분을 위해 사용하는 것이 아니라면 사용하지 않는것이 더 좋다.
이를 함수형으로 구현하게 되면
function query2(obj) {
return Object
.entries(obj)
.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
이렇게도 작성할 수 있다. 이렇게 작성하면 코드가 간결해지고 유지보수에도 용이한 장점이 있다.
그렇다면 a=1, c=CC, d=DD
이 형태를 다시 Object로 만드려면 어떻게 해야 할까? 여기서는 fxjs를 이용한 함수형으로만 작성해보면 코드는 아래와 같다.
const split = _.curry((sep, str) => str.split(sep));
const queryToObject = _.pipe(
split(', '), // ["a=1", "c=CC", "d=DD"]
_.map(split('=')), // [["a", "1"], ["c", "CC"], ["d", "DD"]]
_.map(([k, v]) => ({ [k]: v })), // [{a: "1"}, {c: "CC"}, {d: "DD"}]
_.reduce(Object.assign) // {a: "1", c: "CC", d: "DD"}
)
console.log(queryToObject('a=1, c=CC, d=DD')) // {a: "1", c: "CC", d: "DD"}
처음부터 문자열을 객체로 바꾸려고 생각하면 잘 되지 않기 때문에 코드에 작성되어있는 주석처럼 문제를 작은 단위로 쪼개서 작은 단위부터 문제를 풀어가는 식으로 작성한다면 조금은 수월하게 코드를 작성할 수 있을 것이다.
const a = [['a', 1], ['b', 2], ['c', 3]];
위 작성된 a 변수의 객체를 { a: 1, b: 2, c: 3 }
형태로 바꾸고 싶다면 배열을 객체화 해야한다. 해당 함수는 아래와 같다.
const object1 = entries => _.go(
entries,
L.map(([k, v]) => ({ [k]: v })), // [{a: "1"}, {b: "2"}, {c: "3"}]
_.reduce(Object.assign)
);
const object2 = entries =>
_.reduce((obj, [k, v]) => (obj[k] = v, obj), {}, entries);
console.log(object1(a)); // { a: 1, b: 2, c: 3 }
console.log(object2(a)); // { a: 1, b: 2, c: 3 }
하나는 map, reduce 함수를 이용했고, 다른 하나는 reduce 만으로 작성되었다. 이런식으로 배열을 Object화 하는 이유는 Object는 다형성이 높고 다양한 것들이 배열에 비해 효율적으로 동작이 가능해진다.
예를 들어, [key, value] 형태로 되어있는 배열에서 key 값에 해당하는 value를 알고 싶다면 최악의 경우 모든 배열을 다 순회해야 하지만, 객체에서는 obj[key]
형태로 바로 찾을 수 있다.
object객체에서 value 값마다 순회하며 +10만큼 값을 더하고 싶다면
console.log(mapObject(a => a + 10, { a: 1, b: 2, c: 3 }));
// 위의 결과는 { a: 11, b: 12, c: 13 } 이렇게 될 것이다.
마찬가지로 위와 같이 value에 더하는 것을 어떻게 할지 구상하기 보다 작은 단위로 쪼개서 코드를 작성하는 것이 더 효율적이다.
문제를 작은 단위로 쪼개면서 마지막처럼 결과가 나오길 기대하면 함수를 작성할 때 좀 더 쉽게 접근할 수 있다.
const mapObject = (f, obj) => _.go(
obj, // { a: 1, b: 2, c: 3 }
L.entries, // ['a', 1] ['b', 2] ['c', 3]
_.map(([k, v]) => [k, f(v)]), // [['a', 11], ['b', 12], ['c', 13]]
object // { a: 11, b: 12, c: 13 }
)
console.log(mapObject(a => a + 10, { a: 1, b: 2, c: 3 })); // { a: 11, b: 12, c: 13 }
그 외에도 다양한 함수를 object에 사용할 수 있으며 객체에 다양한 함수를 적용할 수 있게 되면 더 다양한 상황에서 이터러블을 다룰 수 있게되고 그렇게 되면 Javascript에서 거의 모든 것에 대해 순회할 수 있고, 모든 값을 함수로 다룰 수 있게 되므로 코드를 좀 더 효율적이고 유지보수에 용이하도록 작성할 수 있을 것이다.
함수형 프로그래밍을 배우면서 배열을 위주로 코드를 작성했었는데, Object를 사용하면서 함수에 적용시키는 연습을 하니 더 다양한 상황에 적용이 가능해진 것 같다. 지금까지는 명령형으로 코드를 작성하여도 Object는 잘 사용하지 않았다. 사용한다면 코딩 테스트에서 Map, Set 객체 정도??
하지만 함수형 프로그래밍을 공부하면서 연계하여 문자열이나 배열을 Object화 하고, Object에 map 함수를 적용시키는 등 연습을 하면서 이전보다 많이 익숙해진 것 같다.
그리고 함수형 프로그래밍을 처음 접했을 때 상당히 어렵고 작성하기까지 오랜 시간이 걸려서 잘 사용하지 않게 될것이라고 생각했는데, 지금에 와서 과거에 스스로 작성했던 코드를 보거나 지금 내가 명령형으로 작성하는 코드들을 보면 생각보다 복잡하고 코드의 길이도 길며 다른 사람이 봤을 때 이해하기 어려운 부분이 있을 가능성이 높고 수정사항이 생겼을 때 수정하기가 번거롭다는 점이 눈에 보이기 시작했다.
함수형 프로그래밍을 완전히 익히지 않았어도 함수형 프로그래밍을 배우면서 그 외에 명령형, 선언형, 절차형 등 패러다임의 특징들에 대해서 다시 배우게 되는 것 같다.
사실 아직도 함수형 프로그래밍이 익숙하지는 않다. 연습을 통해 작성한 부분들은 모두 간단한 코드들이었고, 실무에서 함수형을 사용해야 하는 상황을 맞딱뜨린다면 아직 자신은 없다. 그래도 함수형 코드들이 처음 학습했을 때에 비해 조금씩 이해되고 있고 그 매력을 알아가는 중이다.
처음에는 어렵고 이해가 안가기만 했던 짧은 코드로 느껴졌었는데 작성할때의 어려움만 제외한다면 나머지 특징들은 모두 장점으로 보이기 시작했다.
나중에 실무에서든 취업을 위해서든 프로젝트를 진행할 때 함수형 프로그래밍이 필요한 순간이 있을 수도 있을 것이다. 그때 뿐만이 아니더라도 계속 공부하면서 함수형 프로그래밍 외에 지식들도 쌓아가고 있음을 느꼈기 때문에 지속적으로 함수형 프로그래밍을 연습하면서 익숙해질 예정이다.