react와 redux의 공식 문서를 읽다 보면 immutability를 강조하는 대목을 종종 볼 수 있다. 프로그래밍을 하다 보면 수정 불가능한 변수를 가끔 볼 수 있는데 java의 string객체를 예로 들 수 있다.
String s = “apple is sweet”;
String s2 = s.replace(“apple”, “banana”);
// 객체 s는 여전히 “apple is sweet”
수정 불가능한 변수는 다양한 장점을 갖고 있는데, 그중 두 가지만 알아보자
- 변수가 수정 불가능하면 함수에서 side effect가 발생할 확률이 낮아진다. side effect가 없는 것만으로도 프로그램의 복잡도가 상당히 줄어든다. 또한 side effect가 없는 함수는 병렬 처리를 효율적으로 할 수 있는데, 빅데이터 분석 프로그램으로 유명한 spark에서 함수형 언어인 scala를 주 언어로 채택한 이유도 여기에 있다.
// side effect 발생 예
var getPlusOneDay = function(date) {
date.setDate(date.getDate() + 1);
return new Date(date);
}
var date1 = new Date(2016, 1, 1);
var date2 = getPlusOneDay(date1);
// 의도치 않게 date1과 date2는 같은 값을 갖는다
- 수정 불가능한 변수는 thread-safe 하므로 동기화 문제에서 자유롭다. 두 개의 스레드가 동시에 같은 변수를 수정하려고 하는 경우를 생각해보면 이해하기 쉽다.
하지만 성능이 중요한 경우에는 수정 가능한 변수를 써야 한다. 예를 들어, 60 fps를 목표로 하는 게임에서 캐릭터의 위치 값을 업데이트하는 경우 매 번 새로운 객체를 생성하는 것보다는 기존 객체의 값을 변경하는 게 더 효율적이다.
수정 불가능한 변수의 일반적인 장점 외에도 react state에서 불변성을 강조하는 진짜 이유는 따로 있다.
React가 화면을 업데이트하는 과정
- setState를 호출 (혹은 부모로부터 props를 전달 받음)
- shouldComponentUpdate를 실행했는데 false를 리턴하면 여기서 멈추고, true를 리턴하면 다음 단계로 이동
- 가상 DOM과 실제 DOM을 비교해서 변경사항이 있으면 화면을 다시 그린다
3번은 react가 알아서 해주는 부분이므로 우리의 관심사는 2번이다.
컴포넌트에서 shouldComponentUpdate 함수를 구현하지 않으면 기본값으로 true를 리턴한다. 모든 컴포넌트에서 shouldComponentUpdate를 구현하는 것보다는 프로그램에 성능 이슈가 생겼을 때 문제가 되는 컴포넌트부터 shouldComponentUpdate를 구현하는 걸 추천한다.
특정 컴포넌트가 업데이트를 할 필요가 없다는 것을 어떻게 판단할 수 있을까? 가장 간단한 방법은 컴포넌트가 갖고 있는 데이터(props, state)의 이전 이후 값을 완전히(?) 비교하는 것이다.
var prevState = {
title: "hello world",
items: [
{
id: 1,
name: "apple"
},
{
id: 2,
name: "banana"
},
...
],
...
};
var nextState = {
title: "hello world",
items: [
{
id: 1,
name: "apple"
},
{
id: 3,
name: "strawberry"
},
...
],
...
};
title을 비교하고, items를 0번 인덱스부터 차례대로 비교하고, 새로운 키가 추가 혹은 삭제됐는지 비교하고… 그렇다 이 방법은 느리다.
수정 불가능한 변수를 사용하면 변수의 레퍼런스만 비교하면 된다.
prevState === nextState
대부분의 경우 위 코드처럼 최상위 오브젝트의 레퍼런스만 비교하면 된다. react에서 addon으로 제공하는 shallowCompare이나 PureRenderMixin에서는 최상위 오브젝트의 레퍼런스가 다른 경우에도 그 하위 오브젝트(1-depth만 더 본다)가 같다면 화면을 다시 그리지 않는다.
React에서 immutable variable을 만드는 방법
javascript에 const 키워드는 있지만 object 내부의 값까지 수정 불가능하게 할 수는 없다. 하지만 몇 가지 쓸만한 방법들은 있다.
방법 1: javascript 표준 함수를 사용
es6의 표준 함수인 Object.assign을 사용하거나 es7에서 표준으로 채택되는 게 거의 확실시되고 있는 object-rest-spread를 사용하는 방법이 있다. Object.assign보다는 babel의 도움을 받아서 object-rest-spread를 사용하는 게 더 낫다.
var person = {
name: "jane",
age: 16,
family: [
{
name: "jack",
age: 15
},
{
name: "tom",
age: 40
},
]
};// object-rest-spread를 사용해서 person을 수정하지 않고 newPerson을 만든다
var newPerson = {
...person,
name: "mike"
};
이 방법은 여러 개의 object가 중첩되어 있을 때 복잡해지는 단점이 있다.
방법 2: immutability-helper를 사용
immutability-helper는 react 공식 문서에서 추천하는 패키지다. 방법 1과는 달리 중첩된 object를 다루기가 쉽다.
// tom의 나이를 39로 변경, 역시 person은 수정되지 않는다
var newPerson = update(person, {family: {1: {age: {$set: 39}}}});
하지만 아직 person object를 직접 수정 가능하다는 위험이 있다.
방법 3: javascript 라이브러리를 사용
여러 라이브러리가 있지만 페이스북에서 만든 immutable-js(react도 페이스북에서 만들었다)를 많이 사용한다.
var immutablePerson = Immutable.fromJS(person);
var newPerson = immutablePerson.updateIn(['family', 1, 'age'], value => 39);
console.log(newPerson.getIn(['family', 1, 'age']));
immutablePerson 변수는 완전히 수정 불가능한 변수가 되었다. 하지만 변수의 값을 읽는 방법이 javascript의 기본 문법과 많이 다르다. 개발이 어느 정도 진행된 프로젝트에서 중간에 immutable-js를 도입하기에는 많은 부담이 된다. 그럴 때는 방법 2가 더 좋은 선택이 될 수 있다.
정리하며
도메인마다 차이는 있겠지만 대부분의 경우 수정 불가능한 변수가 그 반대보다 더 낫다. 자바나 c++에서 클래스를 만들 때도 수정 불가능한 클래스를 만드는 게 더 낫다. 프로젝트 초기에는 못 느끼겠지만 프로그램이 복잡해지면, 도대체 왜 이 변수에 이런 값이 들어갔지? 하면서 열심히 야근을 하게 된다에 500원을 건다.