함수로 협업하는 가장 좋은 방법은 자바스크립트의 기본 객체 혹은 보편적으로 약속된 객체만 값으로 사용해 인자와 리턴값으로만 소통하는 것입니다. 기본 객체란 Array, Object, String 등의 자바스크립트 기본 객체를 말하며 보편적으로 약속된 객체들은 Node.js 에서 가장 많이 쓰이는 웹 프레임워크인 Express.js 의 req, res 등 어떤 프로퍼티와 메서드를 가지고 있는지 충분히 보장된 값입니다.
- 순수 함수
메서드가 자신이 가진 내부의 상태에 따라 다른 결과를 만든다면, 순수 함수는 들어온 인자와 상수만 사용하여 외부 상태를 변경하지 않고 항상 동일한 결과를 반환합니다. 함수가 외부 상태를 변경한다면 부수 효과가 생길 수 있습니다.
부수 효과 문제는 동시성이 생길 때나, 오랫동안 인터랙션을 해서 상태를 지속적으로 관리해야 하는 웹 페이지 혹은 앱에서 문제를 일으키는 경우가 많습니다. 이러한 부수 효과에 대한 문제는 프론트 뿐만이 아니라 서버 측에서도 꽤나 신경쓰이는 부분입니다.
const add1 = (x, y) => x + y; // 순수 함수
const add2 = (x, y) => { // 순수 함수가 아님
x.value = x.value + y;
return x.value;
};
const add3 = (x, y) => x.value + y; // 순수 함수
위 예시에서 add1, add3 은 순수 함수고, add2 는 순수 함수가 아닙니다. x 의 상태를 변경하기 때문입니다.
만약 어딘가 다른 코드 부분에서 x.value 를 사용하고 있다면 이를 굉장히 유의해야하며, 실제로 실무에선 이러한 부분을 신경쓰지 못하고 놓쳐서 버그처럼 보이는 상황이 벌어지곤 합니다.
그렇다고해서 상태 변경을 안 할 수는 없으며, 객체의 값을 변경하는 함수가 없을 수는 없습니다.
다만 상태를 변경하는 것은 목적이 되어야지 어떤 로직을 만들기 위한 수단이 되는 것은 좋지 않습니다.
상태를 변경하면서 결과를 만들어 가는 전략을 선택할 경우 부수 효과가 생길 가능성이 높아집니다.
이는 비단 함수형에만 국한되는 얘기는 아니고 객체지향 프로그래밍에서도 상태를 변경하는 부분과 조회하는 부분을 명확히 나누도록 강조하고 있습니다.
- 불변 객체
직접 값을 변경하는 대신 새로운 값을 만드는 식으로 값을 변경해 나가는 Underscore.js 의 함수들에 대해 알아보도록 하겠습니다.
const users = [
{ name: 'a', age: 30 },
{ name: 'b', age: 29 },
{ name: 'c', age: 26 },
{ name: 'd', age: 34 },
{ name: 'e', age: 31 },
];
const sortedUsers = users.sort((x, y) => x.age - y.age);
console.info(sortedUsers === users); // true
console.info(users.map(item => item.age)); // [26, 29, 30, 31, 34]
console.info(sortedUsers.map(item => item.age)); // [26, 29, 30, 31, 34]
우선 위 코드는 정렬을 할 때 흔히 사용하는 Array.prototype.sort 메서드를 사용해 age 순으로 정렬한 예시입니다.
사실 sortedUsers 를 따로 선언할 필요도 없지만 sort 메서드는 자기 자신을 변경한다는 점을 얘기하기 위한 코드입니다.
=== 로 sortedUsers 와 users 를 비교한 결과를 보면, 두 객체는 그저 동일한 값을 가진 객체가 아닌 완전히 같은 객체입니다.
const _ = require('underscore');
const users = [
{ name: 'a', age: 30 },
{ name: 'b', age: 29 },
{ name: 'c', age: 26 },
{ name: 'd', age: 34 },
{ name: 'e', age: 31 },
];
const sortedUsers = _.sortBy(users, 'age');
console.info(sortedUsers === users); // false
console.info(users.map(item => item.age)); // [30, 29, 26, 34, 31]
console.info(sortedUsers.map(item => item.age)); // [26, 29, 30, 31, 34]
동일한 부분을 Underscore.js 의 _.sortBy 함수를 사용해 정렬해봤습니다.
users 는 원본 그대로이고, 새로운 객체를 반환하는 방식입니다. 그렇기에 부수 효과가 없고, 유리합니다.
비교한 결과 두 배열은 다른 것을 알게 되었는데, 내부 값들도 서로 다를지 확인해보겠습니다.
console.info(sortedUsers[0] === users[2]); // true
정렬을 통해 다르게 보이지만, 두 배열 안에 있는 값들은 새로운 값이 아닌 기존 값입니다.
만약 _.sortBy 로 새로운 객체를 반환할 때 모든 값을 새롭게 만든다면 메모리 사용량이 상당히 높아질 것 입니다.
함수가 변경할 영역에 대해서만 새 값을 만드는 전략은 Underscore.js 의 영리한 전략입니다.
Underscore.js 의 _.reject, _.pluck, _.initial, _.without 과 같은 함수들은 전부 위의 전략을 따르고 있습니다.
const _ = require('underscore');
const user = {
id: 1,
name: 'tom',
age: 40,
licenses: ['history', 'math'],
};
const clonedUser = _.clone(user);
console.info(clonedUser); // { id: 1, name: 'tom', age: 40, licenses: ['history', 'math'] }
console.info(user === clonedUser); // false
clonedUser.age = 50;
console.info(clonedUser.age, user.age); // 50 40
이번엔 Underscore.js 에서 배열이나 객체를 받아 복사하는 _.clone 함수를 사용해봤습니다. 굳이 Underscore.js 가 아니더라도 이런 함수를 제공하는 라이브러리는 많습니다.
위 예시에서 user 와 동일한 내용이 들어있는 clonedUser 라는 새로운 객체가 리턴되었으며, 그 내부의 age 를 변경해도 기존 user 에는 영향을 주지 않습니다. (사실 이런 테스트는 굳이 _.clone 을 쓰지 않고 Object.assign 을 사용해도 가능합니다)
하지만 _.clone 은 객체 내부의 모든 값까지 복사하는 것은 아닙니다. (Object.assign 도 마찬가지입니다)
const _ = require('underscore');
const user = {
id: 1,
name: 'tom',
age: 40,
licenses: ['history', 'math'],
};
const clonedUser = _.clone(user);
clonedUser.licenses.push('science');
console.info(user.licenses); // ['history', 'math', 'science']
console.info(clonedUser.licenses); // ['history', 'math', 'science']
_.clone 함수를 통해 객체가 복사되었지만, 그 내부의 값까지 복사를 하는 것은 아니므로 clonedUser 의 licenses 배열에 값을 추가했을 때 user 의 licenses 까지 영향을 받은 모습입니다. 여기서 중요하게 체크해야 할 점은 자바스크립트의 값과 참조에 대해 정확히 이해하는 것입니다. 자바스크립트에서 참조 객체에 대한 이해가 없어 _.clone 을 하지 않고 코드 작업을 하다가 의도치 않게 객체가 변경되어 있을 수 있고, 단순히 Object.assign (혹은 _.clone) 함수만 사용하고 모든 값이 복사되었겠거니 하며 작업을 진행할 수도 있습니다.
반대로 필요도 없는 부분에 _.clone 을 사용해서 오히려 성능을 저하시킬 수도 있고요.
위와 같은 케이스에선 가장 간단한 해결 방법은 Lodash 의 _.cloneDeep 이라는 함수를 사용하는 것입니다.
const _ = require('lodash');
const user = {
id: 1,
name: 'tom',
age: 40,
licenses: ['history', 'math'],
};
const clonedUser = _.cloneDeep(user);
clonedUser.licenses.push('science');
console.info(user.licenses); // ['history', 'math']
console.info(clonedUser.licenses); // ['history', 'math', 'science']
Lodash 의 _.cloneDeep 함수는 _.clone 과 비슷하지만 재귀적으로 돌면서 그 내부의 값까지 전부 복사합니다.
이를 깊은 복사(Deep Copy) 라고 하며, 이전에 봤던 _.clone 과 같은 동작은 얕은 복사(Shallow copy) 라고 합니다.
왜 객체 복사에 얕고 깊은 차이가 있냐고 하면, 모든 경우에 내부 값을 건드릴게 아니므로 오히려 얕은 복사만 행하는 것이 메모리 사용이나 복사 수행 처리 시간등의 이점을 가져갈 수 있습니다. 결국 위에서 말한 것 처럼, 값과 참조에 대해 정확한 이해가 우선이며 그에 따라 어떤 함수를 사용할 것인지는 부차적인 문제입니다.
- Immutable.js
불변 객체에 대해 얘기하다보면, 이 라이브러리를 뺄 수는 없습니다.
불변 객체란 최초에 값을 만든 후 값을 변경할 수 없는 객체이지만, 사실 자바스크립트에는 불변 객체란 없습니다.
그 컨셉은 오히려 반대로 어떤 객체든 자유롭게 변경하고 확장이 가능하죠.
불변 객체가 없으므로 함수형 프로그래밍을 할 수 없다 는 아닙니다. 불변성은 함수형 프로그래밍의 특성 중 하나일 뿐이며, 자바스크립트에선 이를 불변 객체를 구현한 라이브러리로 대체합니다. 그러한 라이브러리엔 Immutable.js, baobab.js 등이 있습니다.
const Immutable = require('immutable');
const test1 = Immutable.Map({ a: 1, b: 2, c: 3 });
const test2 = test1.set('b', 50);
console.info(test1.get('b')); // 2
console.info(test2.get('b')); // 50
console.info(test1 === test2); // false
위 예시에서 test2 는 Immutable.Map 의 새로운 인스턴스입니다.
값을 변경할 때 그 부분의 값이 변경된 새로운 객체를 리턴하는 식이며 이게 Immutable.js 의 핵심 컨셉입니다.
const Immutable = require('immutable');
const test1 = Immutable.fromJS({ a: { b: { c: [3, 4, 5] } } });
// { a: { b: { c: [3, 4, 5] } } }
const test2 = test1.mergeDeep({ a: { b: { c: [6], d: 6 } } });
// { a: { b: { c: [3, 4, 5, 6], d: 6 } } }
console.info(test1 === test2); // false
위에서 문제가 되었던 부분도 전혀 문제없이 Immutable.js 는 중첩 구조의 데이터를 잘 다룹니다.
깊은 깊이의 값도 찾아가서 확장하거나 변경이 용이합니다. 변경된 부분과 연관된 곳을 제외하고는 기존 객체를 공유하도록 하여 메모리 할당 또한 최소화하도록 구현되어 있습니다. 그 외에도 메모이제이션, List 값 조회 속도 등 아주 좋은 라이브러리입니다.
충분히 이를 활용해 함수형 프로그래밍을 더 우아하게 할 수 있지만, 이러한 라이브러리에도 약간의 단점은 있습니다.
그러한 점을 다음 포스팅에 이어서 얘기하며 자바스크립트의 기본 객체를 다루는 법을 담도록 하겠습니다.
'Backend > 함수형 자바스크립트' 카테고리의 다른 글
더 많은 함수형 자바스크립트 사용.. (1) (0) | 2021.11.28 |
---|---|
기본 객체 다루기 (0) | 2021.11.13 |
지연 평가 (3) (0) | 2021.10.30 |
지연 평가 (2) (0) | 2021.10.23 |
지연 평가 (1) (0) | 2021.10.17 |