Immutable.js는 자바스크립트에서 불변성 데이터를 다룰 수 있도록 도와준다.
Immutable.js를 알기전에 객체 불변성을 알아야 한다.
Immutable.js
객체 불변성을 이해하려면 간단한 자바스크립트 코드를 실행해 보아야 한다. 크롬 웹 브라우저에서 개발자 도구를 열고 다음 코드를 입력해보자
let a = 7;
let b = 7;
let object1 = {a: 1, b:2 };
let object2 = {a: 1, b:2 };
보다시피 a 값과 b값은 같다.
둘은 === 연산자로 비교해 보면 당연히 true를 반환할 것이다.
하지만 object1과 object2가 가진 값이 같더라도 서로 다른 객체이기 때문에 둘을 비교하면 false를 반환한다
object1 === object2;
// false
다음 코드로는 어떨까
let object3 = object1;
object1 === object3;
// true
object3에 object1을 넣고, 두 값을 비교하면 true를 반환한다. object1과 object3은 같은 객체를 가리키기 때문이다.
그렇다면 다음 코드를 실행하고 나서 비교하면 어떨까?
object3.c = 3 ;
object1 === object3
//true
object1
//object { a: 1, b:2, c:3 }
object1에도 c값이 생성되었다
그렇다면 다른 예제로 다음코드는 어떨까
let array1 = [0,1,2,3,4];
let array2 = array1;
array2.push(5);
이렇게 array2에 5를 상비하고, array1과 array2를 비교하면 무엇이 나올까
array1 == array2
//true
이번에도 true를 반환
리액트 컴포넌트는
state또는 상위 컴포넌트에서 전달받은 props 값이 변할 때 리렌더링되는데,
배열이나 객체를 직접 수정한다면 내부 값을 수정했을지라도 레퍼런스가 가리키는 곳은 같기 때문에
똑같은 값으로 인식 한다.
이런 이슈 때문에 지금까지 여러 층으로 구성된 객체 또는 배열을 업데이트해야 할 때,
전개 연산자(...)를 사용해서 기존 값을 가진 새 객체 또는 배열을 만들었던 것이다.
하지만 그렇게 작업하다 보면 간단한 변경을 구현하는 데도 코드가 복잡할 때가 있다.
예를 들어 수정해야 할 값이 객체의 깊은 곳에 위치한다면
다음 형식으로 해야된다.
기존 방식
let object1 = {
a : 1,
b : 2,
c : 3,
d : {
e : 4,
f : {
g : 5,
h : 6
}
}
};
//h 값을 10으로 업데이트 한다.
let object2 ={
...object1,
d : {
...objec1.d,
f : {
...object1.d.f,
h: 10
}
}
}
객체를 불변성을 유지할 필요가 없다면 다음과 같이 간단하게 해도 된다.
object1.d.f.h = 10;
배열을 다룰때도 마찬가지인데
배열 안에 있는 값을 수정하려면 수정하려는 원소 위치를 전후를 slice로,
가져와야 하는데 꽤 귀찮은 작업이다.
이런 작업들을 간소화하려고 페이스북에서 만든 라이브러리 Immutable.js가 있다.
이 라이브러리 사용하면 이 코드는 다음 형식으로 작성 할 수 있다.
let object1 = Map({
a: 1,
b: 2,
c: 3,
d: Map({
e: 4,
f: Map({
g: 5,
h: 6
})
});
});
let object2 = object1.setIn([ 'd', 'f', 'h'], 10);
object1 === object2;
//false
Immutable의 Map은 객체 대신 사용하는 데이터 구조이다.
자바스크립트에 내장된 Map과 다름
CDN을 이용하여 불러온다. JSBin(https://jsbin.com/)
Html (CDN)
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.7.3/immutable.min.js"></script>
리액트에서는 라이브러리에서 불러온다.
import { Map, fromJS } from 'immutable';
const {Map} = Immutable;
const data = Map({
a:1,
b:2
});
Map을 사용할 때는 Map 함수 안에 객체를 넣어서 호출 한다.
이번에는 여러 층으로 구성된 객체를 Map으로 만들어 보자
const {Map} = Immutable;
const data = Map({
a:1,
b:2,
c: Map({
d:3,
e:4,
f:5
})
});
이 처럼 객체 내부에 또는 다른 객체들이 있다면 내부 객체들도 Map으로 감싸 주어야 나중에 사용하기 편하다.
( 내부 객체들도 Map을 필수로 써야 하는 것은 아니지만,
내부에서 Map을 사용하지 않으면 추 후 setIn, getIn을 활용 할 수 없다.)
객체 내용을 네트워크에서 받아 오거나 전달받는 객체가 너무 복잡한 상태라면 일일이 그 내부까지 Map으로 만들기 힘들수도 있다.
이때는 fromJS를 사용할 수 있다.
const {Map, fromJS} = Immutable;
const data = fromJS({
a:1,
b:2,
c: {
d:3,
e:4,
f:5
}
});
fromJS를 사용하면 이 코드처럼 내부에 있는 객체들은 Map을 쓰지 않아도 된다.
Immutable로 Map을 만들어 주었는데, 이를 콘솔에 프린트하면 어떻게 나오는지 확인해 보자.
const {Map, fromJS} = Immutable;
const data = Map({
a:1,
b:2,
c: Map({
d:3,
e:4,
f:5
})
});
console.log(data);
console.log(data);를 해보면 객체 정보가 매우 길게 나타난다.
여기에서 나타나는 것들은 Immutable 데이터 가진 내부 변수 및 함수들이다.
해당 데이터를 실제로 활용하거나 업데이트를 해야 할 때는 내장 함수를 사용해야 한다.
예를 들어 data내부의 a값을 참조하고 싶다면
data.a로 작성하는 것이 아니라, data.get('a')를 해야 한다.
Immutable 객체에 내장된 함수들은 종류가 매우 많은데,
자주 사용하는 것 위주로 알아보겠다.
Immutable 객체를 일반 객체 형태로 변형하는 방법은 다음과 같다.
const deserialized = data.toJS();
console.log(deserialized);
//{a: 1, b: 2, c: { d :3, e :4 }}
특정 키의 값 불러오기
특정 키의 값을 불러올 때는 get 함수를 사용합니다.
data.get('a'); //1
깊숙이 위치하는 값 불러오기
Map 내부에 또 Map이 존재하고, 그 Map 안에 있는 키 값을 불러올 때는 getIn 함수를 사용한다.
data.getIn(['c' , 'd' ]); //3
새 값을 설정할 때는 get 대신 set을 사용한다.
const newData = data.set('a', 4);
set을 한다고 해서 데이터가 실제로 변하는 것은 아닌다.
주어진 변화를 적용한 새 Map을 만드는 것이다.
console.log(newData === data);
서로 다른 Map이기 때문에 false를 프린트한다.
기존 data값은 그대로 남아 있고, 변화가 적용 된 데이터를 newData에 저장하는 것이다.
깊숙이 위치하는 값 수정 ( setIn )
깊숙이 위차하는 값을 수정할 때는 setIn을 사용한다.
이때 내부에 있는 객체들도 Map 형태여야만 사용할 수 있다는 점에 주의해야 한다.
const newData = data.setIn(['c','d'],10);
여러 값 동시에 설정 ( marge )
값 여러 개를 동시에 설정해야 할 때는 mergeIn를 사용한다.
예를 들어 c값과 d값, c값과 e값을 동시에 바꾸어야 할 때는 코드를 다음과 같이 입력한다.
[ 방법 1 ]
const newData = data.mergeIn(['c'], { d : 10, e : 10})
이렇게 mergeIn를 사용하면 c안에 들어 있는 f값은 그대로 유지하면서 d값과 e값만 변경한다.
또는 코드를 다음과 같이 입력 할 수도 있다.
[ 방법 2 ]
const newData = data.setIn(['c', 'd'], 10)
.setIn(['c', 'e'], 10);
그리고 최상위에서 merge를 해야 할 때는 코드를 다음과 같이 입력한다.
const newData = data.marge({a : 10 , b : 10});
즉, set을 여러번 할지, 아니면 merge를 할지는 그때그때 상황에 맞춰 주면 되지만,
성능상으로 set을 여러번 하는것이 빠르다
( 하지만 애초에 오래 걸리는 작업이 아니므로 실제 처리 시간의 차이는 매우 미미하다.)
Immutable 데이터 구조로 배열 대신 사용한다.
배열과 동일하게 map. filter, sort, push, pop 함수를 내장하고 있다.
이 내장 함수를 실행하면 List 자체를 변경하는 것이 아니라, 새로운 List를 반환하는 것을 꼭 기억하길
또 리액트 컨포넌트는 List 데이터 구조와 호환되기 때문에 map 함수를 사용하여 데이터가 들어있는 List를
컴포넌트 List로 변환하여 JSX에서 보여주어도 제대로 렌더링 된다.
생성
const { List } = Immutable;
const list = List( [0,1,2,3,4 ] );
객체들을 List를 만들어야 할때
객체들을 Map으로 만들어야 추후 get과 set을 사용 할 수 있다.
const { List, Map, fromJS } = Immutable;
const list = List([
Map({value: 1}),
Map({value: 2})
]);
// or
const list2 = fromJS([
{value:1},
{value:2}
]);
fromJS를 사용하면 내부 배열은 List로 만들고, 내부 객체는 Map으로 만든다.
그리고 Map과 마찬가지로 List도 toJS를 사용하여 일반 배열로 반환할 수 있다.
이 과정에서 내부에 있는 Map들도 자바스크립트 객체로 변환된다.
console.log(list.toJS());
n번째 원소 값은 get(n)을 사용하여 읽어 온다.
list.get(0);
0번째 아이템의 value값은 다음과 같이 읽어 온다.
list.get([0, 'value']);
const newList = list.set(0, Map({vlaue:10}))
List의 Map 내부 값을 변경하고 싶을 때는 다음과 같이 setIn을 사용 한다.
const newList = list.setIn([0, 'vlaue'],10);
다음 방법으로는 update를 사용할 수도 있다.
const newList = list.update(0, ,item => item.set('value', item.get('value') * 5 ) )
값을 업데이트해야 하는데 기존 값을 참조해야 할 때는 이처럼 update를 사용하면 편하다.
첫번째 파라미터는 선택할 인덱스 값
두번째 파라미터는 선택한 원소를 업데이트하는 함수
update를 사용하지 않았다면 다음과 같이 작성해야된다.
const newList = list.setIn(0,'value'), list.getIn([0, 'value'] * 5 )
아이템을 추가할 때는 push를 사용한다.
이 함수를 사용한다고 해서 Array처럼 기존 List자체에 아이템을 추가하는 것은 아니다.
새 List를 만들어서 변환하므로 안심하고 사용할 수 있다.
const newList = list.push(Map({value: 3}))
리스트 맨 뒤가 아니라 맨 앞에 데이터를 추가하고 싶다면 push대신 unshift를 사용해야한다.
const newList = list.unshift(Map({value: 3}))
const newList = list.delete(1);
이렇게 작성하면 인덱스 1인 아이템을 제거한다.
Array가 가진 내장 함수를 List도 대부분 가지고 있다.
예를 들어 마지막 아이템을 제거하고 싶다면 pop을 사용해도 된다.
const new List = list.pop();
List에서는 length가 아니라 size를 참조해야 한다.
console.log(list.size);
비어있는지 확인하고 싶다면 .isEmpty()를 사용 할 수 있다.
console.log(list.isEmpty());