react에서 state는 항상 읽기 전용이어야 하며, array 등의 reference value들은 새로운 reference를 만들어서 state를 변경해 주어야 한다.
이와 같은 방식으로 redux에서도 state는 읽기 전용으로 관리 되어야 한다. 그래서 spread operator (...)
를 사용하거나 concat
Object.assign
등의 새로운 reference를 만드는 메소드를 사용하기도 한다.
state를 읽기전용으로 이용해야 하는 이유는 데이터 변경을 감지하는 방식을 얕은 검사?(shallow equality checking
) 로 하기 때문이다.
쉽게 이야기하면 redux는 변경되는 데이터를 단순하고 쉽게 감지하고 다시 렌더링하는 기술을 사용하고 있기 때문에 단순 비교를 통해 좋은 성능을 유지할 수 있다.
다음의 링크에서 더 자세한 정보를 알 수 있다.
https://redux.js.org/faq/immutable-data#how-redux-uses-shallow-checking
Immutable.js는 state의 불변성을 유지시켜주면서 redux의 state를 변경하는 과정을 간편하게 만들어 준다.
아래의 리듀서 함수를 예로 들어보자.
src/reducers/currentChat.js
const initialState = {
chats: [],
isLoadingInitialChats: true,
isFetchInitDataError: false
};
export default function entireChatList(state = initialState, action) {
switch (action.type) {
case REQUEST_INIT_CHAT_LIST:
return Object.assign({...state}, {
isLoadingInitialChats: true
});
default:
return state;
}
}
채팅 리스트를 요청하는 액션에 따라서 state를 변경하는 리듀서 함수이다. 기존 state의 정보를 변경하여 새로운 state를 만들어서 리턴해야 하므로, Object.assign이라는 메소드와 spread operator를 사용하고 있다.
이와 같은 과정을 immutable.js로 직관적으로 만들 수 있다.
Immutable.js에서 새로 생성하여 배정하고자 하는 대상(위의 예시에서는 state 객체)에 따라 규칙이 달라진다. 객체(Object)를 대상으로 할 때는 Map을 이용하면 된다. Immutable.js Map
Map으로 정의된 객체는 Immutable 메소드를 이용하여 읽기(get), 쓰기(set), 갱신(update) 등을 할 수 있다. 이 때, 쓰기(set) 메소드는 새로운 Map을 리턴하게 된다. 이러한 특성 때문에 Immutable.js 를 사용하면 별도의 매핑을 하지 않아도 새로운 객체를 반환시킬 수 있다.
Initial state를 MAPPING하기
Immutable.js의 Map의 사용법은 간단하다.
const { Map } = require('immutable');
const obj = Map({ 0: "value" });
이런식으로 객체를 Map함수의 인자로 객체를 넣게 되면 해당 객체를 Immutable하게 다룰 수 있게 된다.
여기서 주의할 점은 key값을 String으로 적어야 한다는 것이다.
let obj = { 0: "value" };
obj[0] === obj["0"]; // true
let map = Map(obj);
map.get("0") === map.get(0) //false
//map.get("0") === "value"
//map.get(0) === undefined
위의 예시를 보면 javascript에서 객체의 키값은 자동으로 String으로 변환되어 value를 얻어낼 수 있다. 하지만 Immutable.js에서는 get 메소드를 통해 value를 추출하기 위해서는 String 값으로 정확히 key를 적어주어야 한다.
그러면 currentChat 예시에서 initialState를 Mapping 해보자. 다음과 같이 작성하면 된다.
const { Map } = require('immutable');
const initialState = Map({
chats: [],
isLoadingInitialChats: true,
isFetchInitDataError: false
});
initialState를 Map으로 만들었으니 이젠 state 변경시마다 Map을 활용하여 항상 새로운 객체를 리턴하는 함수를 만들 수 있다.
action 마다 Map을 리턴하도록 만들기
Map을 통해 새로운 객체를 만들어 state를 변경하는 방법은 2가지가 있다. 1) 리듀서 함수에서 새로운 Map을 리턴하고 Container에서 객체로 반환하여 이용하는 방법, 2) 리듀서 함수 자체에서 새로운 Map을 만들고 새로운 객체까지 반환하여 state를 변경하는 방법.
이 중에 1)번 방법이 더 좋을 것 같다. 2)번 방식을 사용했을 때는 액션을 추가하거나 변경할 때마다 객체 리턴까지 생각하여 코드를 작성해야 하기 때문에 더욱 번거로울 것 같다. 그리고 객체로 리턴하는 코드를 한 쪽에서 일괄 담당하는 것이 더 효율적이라고 생각한다.
먼저 REQUEST_INIT_CHAT_LIST
의 경우, state의 isLoadingInitialChats
key 값을 변경하여 리턴하여야 한다. 기존에 있는 key의 value를 변경할 때는 update 메소드를 활용하여 새로운 map을 리턴할 수 있다. Immutable.js update
const { update } = require('immutable');
const newMap = state.update('isLoadingInitialChats', value => true);
//update('key', updater(function));
위와 같은 구문은 새로운 map을 리턴하여 받는다. 동일한 방식으로 acrion마다 Map을 리턴하도록 만들 수 있다.
Map으로 만들 객체를 읽어오기
Immutable.js를 사용할 때 약간의 단점이 기존 객체처럼 key 값에 접근하지 못한다는 것이다. 위의 리듀서 함수를 Map으로 만들면 그 state 값을 읽어올 때도 Map의 메소드를 활용하여서 가져와야 한다. 위에서 update한 key의 value를 읽어오기 위해서는 다음과 같은 코드를 작성해야 한다.
const { get } = require('immutable');
const isLoadingInitialChats = get(state, 'isLoadingInitialChats');
//get(map, key)
객체를 읽어오는 과정에서 유의할 점은 key값을 반드시 String으로 동일하게 작성해 주어야 한다는 것이다. state 자체를 map으로 관리하는 상황이라면 state를 받아 props로 넘겨주는 Container 에서 위의 작업으로 value를 추출해야 할 것이다.
state의 요소에는 object 뿐만 아니라 배열들도 있을 수 있다. 배열에 요소를 추가하여 state를 갱신하고 싶은 경우, 새로운 배열을 만들어서 다시 배정을 해주는 작업은 생각보다 손이 많이 간다.
Object의 map과 마찬가지로 Array 유형의 경우 List로 관리할 수 있다. 세부 메소드들은 비슷하므로 따로 설명은 생략하고 링크고 대체하겠다.
https://immutable-js.github.io/immutable-js/docs/#/List
react와 redux를 사용하면서 Immutable이라는 속성에 많은 귀찮음을 느꼈는데... 제약 조건들(state를 직접 건드리면 안된다, state는 불변해야 한다 등등..) 을 어느 정도 해소해주는 데에 의의가 있는 것 같다.