최근 Immutable js 을 사용할 일이 생겨 이를, 간단하게 정리해 보고자 한다.

객체 지향 프로그래밍에 있어서 불변객체 (Immutable Object) 는 생성 후 그 상태를 바꿀 수 없는 객체를 말한다.

Immutable js 는 Javascript 의 값을 불변객체 로 만들 수 있게 도와준다.

Immutable js 간단 예제

// javascript
import { Map, List, is, fromJS } from 'immutable';
const map1 = fromJS({ a: {b: 1, c: 2}, d: 3});
const map2 = Map({ a: Map({b: 1, c: 2}), d: 3});
const map3 = map1.set('d', 3);
console.log(map1 !== map2);
console.log(map1 === map3);
console.log(map1.equals(map2));
console.log(map1.toJS().a.b === map3.getIn(['a', 'b']))
const list1 = List([ 1, 2, Map({}) ]);
const list2 = list1.push(3);
console.log(list1 !== list2);
console.log(list1.equals(list2.pop()));

Immutable js 는 {} 와 [] 대신 Map 과 List 라는 새로운 객체를 제공한다.

이 객체 들은 추가, 수정, 삭제 할 때, 반드시 새로운 객체를 반환하는데, 값이 달라지지 않았다면, 동일한 객체를 반환한다.

이는 Immutable js 가 마치 Virtual DOM 과 같은 Tree 구조체를 형태로 이루어져 있으며, 변화가 일어나는 구조체 (Collection) 는 새로 만들고, 나머지는 재 사용하는 방식으로 메모리 사용량을 줄이고, 동작 속도를 향상 시킨다.

Immutable js 객체

Immutable js 는 Collection 이라는 추상 클래스를 가진다.

Collection 은 Keyed, Indexed, Set 으로 확장되어 사용된다.

아래의 Class 들을 가진다.

  • Map
    Object: key 와 value
    Map extends Collection.Keyed
  • List
    Array
    List extends Collection.Indexed
  • Stack
    Stack 형태로 동작하지만 Array 의 동작의 대부분 사용 가능하다.
    Stack extends Collection.Indexed
  • Set
    Set: value 와 value
    Set extends Collection.Set
  • Seq
    Collection 의 Lazy evaluation (지연 평가) 을 제공한다.
    Seq extends Collection
  • Range
    시작과 종료 값을 받아 Infinity Array 를 제공한다
    Range extends Seq.Indexed
  • Repeat
    값을 받아 Infinity Array 를 제공한다
    Repeat extends Seq.Indexed
  • Record
    Record 는 기본값을 기억하는 Record.Factory 를 반환한다.
    이 Factory 는 값을 입력받아 새로운 Record 객체를 반환한다.

Collection 은 값을 조작 하기 위해 공통적인 Method 를 제공한다.

값 가져오기 Get, GetIn

get 은 key 값을 받아 접근 할 수 있다.

getIn 은 key 값을 Iterable 로 받아 값을 접근 할 수 있다.

만약 깊이와 관계없이 값을 찾지 못하면 undefind 가 반환된다.

// javascript
collection.get('key');
collection.getIn(['key1', 'key2']);

값 세팅하기 Set, SetIn, update, updateIn

set, setIn, update, updateIn 은 값이 같지 않다면, 항상 새로운 Class 를 반환한다.

set 와 update 의 차이점은 update 는 함수를 입력받아 Class 의 값을 조작할 수 있게 해준다.

// javascript
collection.set('key', 'value');
collection.setIn(['key1', 'key2'], 'value');
collection.update('key', (value) => value);
collection.updateIn(['key1', 'key2'], (value) => value);

Class 합치기 merge, mergeDeep, mergeDeepWith

merge, mergeWith, mergeDeep, mergeDeepWith 은 이전 Class이후 Class 를 합쳐, 항상 새로운 Class 를 반환한다.

merge 와 mergeDeep 의 차이점은 merge 는 자식 Class 은 merge 진행하지 않고 변경되고, mergeDeep 은 자식 Class 도 merge 를 진행한다.

mergeWith 와 mergeDeepWith 으로 비교함수를 입력받아, key 가 충돌이 일어날 때 반영할 값을 지정할 수 있다.

// javascript
const one = Map({ a: 10, key1: Map({ key2: 5 }) });
const two = Map({ a: 40, key1: Map({ key3: 5 }), c: Map({ z: 3 }) })
one.merge(two);
one.mergeWith((oneV, twoV) => twoV, two);
one.mergeDeep(two);
one.mergeDeepWith((oneV, twoV) => twoV, two);
one.mergeIn(['key1', 'key2'], two)
one.mergeDeepIn(['key1', 'key2'], two)

Class 비교하기 is 와 equery

is, equery 는 Class 의 값을 비교한다.

이때 자식 Class 도 모두 비교한다.

// javascript
const map1 = Map({ a: 1, b: List([1, 2]), c: Map({d: Map({ e: 1 }) }) });
const map2 = Map({ a: 1, b: List([1, 2]), c: Map({d: Map({ e: 1 }) }) });
console.log(false, map1 === map2,);
console.log(false, Object.is(map1, map2));
console.log( true, is(map1, map2));
console.log( true, map1.equals(map2));

지연평가 withMutations 와 Seq

Immutable js 의 Class 는 변화가 일어나면, 반드시 새로운 Class 를 반환한다.

항상 새로운 Class 는 더 많은 메모리와 더 많은 연산을 가져와 성능에 좋지 않다.

연산 중 새로운 Class 생성과 계산을 늦출수록 성능에 도움이 된다.

이를 지연평가 (Lazy evaluation) 라고 부른다.

withMutations 는 이 Class 의 생성을 잠시 밀어두어, 성능을 올릴 수 있다.

// javascript
const MAX = 100000;
let i = 0;
console.time();
let list1 = List([]);
i = 0;
while (i < MAX) {
list1 = list1.push(i++);
}
console.timeEnd();
// default: 65ms
console.time();
let list2 = List([]).withMutations((list) => {
let i = 0;
while (i < MAX) {
list.push(i++);
}
})
console.timeEnd();
// default: 15ms
console.time();
let list3 = Range(0, MAX)
console.timeEnd();
// default: 0ms

Seq 는 Lazy operation 을 지원한다.

Seq 는 생성 후 수정이 불가능 하지만, operation chaining 과정에서 새로운 Class 를 생성하지 않고, 필요한 만큼만 연산하기 때문에, 성능을 올릴 수 있다.

// javascript
Seq([1,2,3,4,5])
.map(n => n + 1)
// [2,3,4,5,6]
.filter(n => n % 2 === 0)
// [2,4,6]
.take(2)
// [2,4]
.reduce((acc, item) => acc + item, 0)
// 6

Range 와 Repeat 는 필요한 만큼 반복하는 Seq 를 만들 수 있다. 이때 값들은 호출 즉시 생성되지 않고, 지연 생성되기 때문에, 성능을 올릴 수 있다.

// javascript
const MAX = 100000;
let i = 0;
let list = [];
while (i < MAX) {
list.push(i++);
}
console.time();
list.map(n => n + 1).reduce((acc, item) => acc + item, 0)
console.timeEnd();
// default: 6ms
console.time();
List(list).map(n => n + 1).reduce((acc, item) => acc + item, 0)
console.timeEnd();
// default: 43ms
console.time();
Seq(list).map(n => n + 1).reduce((acc, item) => acc + item, 0)
console.timeEnd();
// default: 7ms
console.time();
Range(0, MAX).map(n => n + 1).reduce((acc, item) => acc + item, 0)
console.timeEnd();
// default: 7ms

Class 변환하기 fromJS 와 toJS

Immutable js 에서 Map 이나 List 로 직접 생성 할 때, 자식 Class 는 자동으로 변화시키지 않는다.

fromJS 을 사용하면 Javascript 객체나 배열을 Immutable js 객체로 쉽게 변화시킬 수 있다.

이때 Array 는 List 로 변화시키고, Object 는 Map 으로 변화시킨다.

Immutable js 객체 또한 Javascript 객체 로 변환할 수 있다.

  1. toJSON
  2. toArray
  3. toObject
  4. toJS

이때 toJSON, toArray, toObject 는 자식 Class 는 변화시키지 않는다.

toJS 는 모든 Class 를 돌면서 Javascript 객체로 변환 하는데, 때문에 비싼 성능비용을 가진다.

// javascript
const map1 = fromJS({a: 1, b: { c: 2, d: 3}, e: [4, 5, 6]});
const map2 = Map({a: 1, b: Map({ c: 2, d: 3}), e: List([4, 5, 6])});
map1.toJS();
map2.get('e').toArray();

Immutable js 의 장점과 단점

장점

  • 다양한 불변 객체를 쉽게 생성하고, 관리 할 수 있다.
  • 코드가 간결해지고, 방어적 복사가 쉬워진다.
  • 유용하고 다양한 operation 을 제공한다.
  • 불변 객체로 이전 객체의 변화를 신경 쓰지 않아도되 SideEffect 가 줄어든다.
  • 불변 객체를 항상 생성하지 않고, 필요할 때마다 생성하고, 각 객체마다 공유하여, 성능을 높인다.
  • 지연평가 (Lazy evaluation) 를 지원하여 성능을 높인다.

단점

  • 학습 비용이 높다.
  • 항상 불변 객체를 반환하므로, 메모리 사용률이 높다.
  • Javascript 객체로 변환할 일이 많이 생기는데, 이 변환 비용이 많이 든다.
  • 때문에 Javascript 객체 대신 사용될 일이 많아 프로젝트의 Immutable js 의 의존도가 늘어나, 추후에 전환 비용이 비싸진다.

Javascript 와 불변 객체 (Immutable Object)

함수형 프로그래밍에서는 값을 항상 불변 상태를 유지한다.

함수 내에서는 객체의 상태를 변화시키지 않고, 항상 새로운 객체를 반환한다.

때문에, 이전에 변경되었던 사항을 신경 쓸 필요가 없으며, 항상 현재 상태만을 고민하면 되기 때문에, 복잡함이 없어진다.

불변객체 는 흐름 기반 프로그래밍이 가능하게 하는 첫 번째 요소이다.

Javascript 의 원시 값들은 불변하기 때문에, 1 + 2 나 ‘Hello’ + ‘World’ 같은 연산자는 항상 새로운 값을 반환한다.

Object 는 큰 내용은 Memory 에 두고, 포인터 같은 작은 참조 값으로 내용에 접근한다.

큰 내용 대신 작은 참조 값을 주고받기 때문에, 내용을 주고 받을 때에 비해 메모리 용량이 절감되고, 성능이 좋아지게 된다.

하지만 이런 Object 의 속성 값은 전달 과정에서, 언제든지 변경될 수 있다.

// javascript
var obj = {
a: 10,
b: 20,
};
function changeObj(obj) { obj.a = 30 }
changeObj(obj);
console.log(obj.a === 30);
var array = [1, 2, 3];
function changeArray(array) { array.push(4) }
changeArray(array);
console.log(array.length === 4);

Javascript 에서는 Object 의 상태 조작을 의도적으로 막기 위해서, Object.defineProperty() 와 Object.freeze() 를 제공한다.

Object.defineProperty 와 Object.freeze

Object.defineProperty 은 해당 객체의 속성의 수정을 writable 로 막을 수 있다.

// javascript
var obj = {
value: 'value'
};
Object.defineProperty(obj, 'value', {
writable: false
});
// update
obj.value = 'changeValue';
console.log(obj.value === 'value');

Object.freeze 는 해당 객체를 추가, 삭제, 수정을 막아 불변성을 제공할 수 있다.

// javascript
var obj = {
value: 'value'
};
Object.freeze(obj);
Object.isFrozen(obj);
// add
obj.addValue = 'addValue';
console.log(obj.addValue === undefined);
// update
obj.value = 'changeValue';
console.log(obj.value === 'value');
// delete
delete obj.value;
console.log(obj.value === 'value');
var arr = [1,2,3];
Object.freeze(arr); // 이제 배열을 수정할 수 없음.
// add
arr.push(4);
console.log(arr.length === 3);
// update
arr[0] = 4;
console.log(arr[0] === 1);
// delete
delete arr[0];
console.log(obj.value === 'value');

하지만 Object.freeze 는 속성 값의 변경은 막지만 참조값이 가르키는 객체와 prototype 의 변경은 막지 못한다.

// javascript
var obj = {
refer: {
value: 'value';
}
};
Object.freeze(obj);Object.prototype.addValue = 1;
obj.refer.value = 2;
console.log(obj.addValue === 1);
console.log(obj.refer.value === 2);

Immutable js 는 다루기 어려운 Javascript 구조체를 쉽게 Immutable Object 로 만들고 조작할 수 있다.

Javascript is great We may not be great

Javascript is great We may not be great