본문 바로가기

React.js

React(리액트): immer 라이브러리 사용한 불변성 관리

반응형

이번에 학습할 내용은 'immer'라는 라이브러리를 사용해 더 쉽게 불변성을 지키는 방법이다.

 

const object = {
a: 1,
b: 2
};
object.b = 3;

다음과 같이 object라는 객체가 있다고 가정해보자. 우리가 리액트에서 배열이나 객체를 업데이트해야 할 때에는 다음 코드처럼 값을 직접 수정하는 형태의 코드는 불변성을 깨뜨리는 형태의 코드이다. 

const object = {
a: 1,
b: 2
};
const nextObject = {
...object,
b: 3
}

그 대신에 새로운 객체를 만들고 spread 연산자를 사용해서 기존의 값을 집어넣고 새로운 값을 덮어쓰는 방식이 올바른 방식이다. 이렇게 해야 나중에 컴포넌트가 제대로 리 렌더링 되고 컴포넌트 최적화도 수행할 수 있다. 

 

const todos = [
{
id: 1,
text: '할 일 #1',
done: true
},
{
id: 2,
text: '할 일 #2',
done: false
}
];
todos.push({
id: 3,
text: '할 일 #3',
done: false
});
todos.splice(
todos.findIndex(todo => todo.id === 2),
1
);
const selected = todos.find(todo => todo.id === 2);
selected.done = !selected.done;

배열도 마찬가지로 push, splice와 같은 함수를 사용하거나 n번째 항목을 직접 수정하면 안된다.

 

const todos = [
{
id: 1,
text: '할 일 #1',
done: true
},
{
id: 2,
text: '할 일 #2',
done: false
}
];
const inserted = todos.concat({
id: 3,
text: '할 일 #3',
done: false
});
const filtered = todos.filter(todo => todo.id !== 2);
const toggled = todos.map(
todo => todo.id === 2
? {
...todo,
done: !todo.done,
}
: todo
)

그 대신에 우리는 concat, filter, map과 같은 함수를 사용해서 새로운 배열을 만들어내야 한다. 

 

대부분의 경우는 이렇게 spread연산자와 배열의 내장 함수를 사용해서 처리하는 것은 그렇게 복잡한 일은 아니다.

하지만 상태가 복잡해지면 불변성을 지키면서 상태를 만들어내는 게 꽤나 복잡해진다. 

 

const nextState = produce(state, draft => {
const post = draft.posts.find(post => post.id === 1);
post.comments.push({
id: 3,
text: '와 정말 쉽다!'
});
});

우리가 앞으로 'immer'라는 라이브러리를 사용하면 함수를 다음과 같이 작성할 수 있다.

draft에다가 우리가 불변성을 신경 쓰지 않고 바로 값을 바꿔줄 수가 있는데 코드를 살펴보면 posts 배열에 있는 특정 post를 id를 이용해 찾고  그다음에 해당 post가 갖고 있는 comments에다가 바로 push 해 줄 수 있다. 

 

immer를 사용하면 불변성을 해치는 코드를 작성해도 대신 불변성 유지를 해준다.

 

immer를 사용하기 위해서는 git bash에서 'yarn add immer'를 입력해 설치해주어야 한다.

그리고 상단에 'import produce from immer' 코드를 입력해 import 해준다.

 

크롬 개발자 도구에서 immer 사용법을 익혀보자.

produce함수를 사용해서 state의 값을 바꾸어 주었고 바꾼 결과를 nextState에 저장했다.

produce의 첫 번째 파라미터에는 우리가 바꾸어주고 싶은 배열이나 객체를 넣어주면 되고 두 번째에는 어떻게 바꾸어 줄 것인지에 대한 함수를 넣어주면 된다. 이 함수에서는 draft라는 값을 파라미터로 받아와서 그 내부에서 하고 싶은 

작업을 하면 되는데, state에 있는 number 값을 1씩 올려주도록 했다.

여기서 보면 draft함수에서는 state를 참조한 것이 아니라 draft를 참조해 number값을 바꾸어 주었다. draft가 가지고

있는 값은 state랑 똑같은데, 이렇게 draft에 변화를 주게 되면 불면성을 지키면서 새로운 객체를 만들어준다.

nextState의 number가 2로 바뀐 것을 확인할 수 있다. produce함수가 알아서 불변성을 유지해주는 것이다.

 

배열에 push를 해주는 것도 가능하다. produce함수에서는 draft를 가져와 array를 어떻게 바꾸어 줄지 작성해주었다.

nextArray가 draft에 따라 바뀐것을 확인할 수 있다.

 

이렇게 immer를 사용하면 불변성을 쉽게 유지할 수 있다.

 

그럼 이제 App 컴포넌트의 reduce에서 immer를 사용해보도록 하자.

미리 말을 하자면, immer를 사용해서 간단해지는 업데이트가 있고, 오히려 코드가 길어지는 업데이트들이 있다.

예를 들어 우리가 만들었던 프로젝트 상태의 경우 users 배열이 객체 깊은 곳에 위치하지 않기 때문에 새 항목을

추가하거나 제거할 때는 immer를 사용하는 것보다 concat과 filter를 사용하는 것이 더 코드가 짧고 편하다.

하지만, 사용법을 배워보기 위해 immer를 사용하여 처리를 해주도록 하겠다.

 

App.js

import React, { useReducer, useMemo } from 'react';
import produce from 'immer';
import UserList from './UserList';
import CreateUser from './CreateUser';
function countActiveUsers(users) {
console.log('활성 사용자 수를 세는중...');
return users.filter(user => user.active).length;
}
const initialState = {
users: [
{
id: 1,
username: 'velopert',
email: 'public.velopert@gmail.com',
active: true
},
{
id: 2,
username: 'tester',
email: 'tester@example.com',
active: false
},
{
id: 3,
username: 'liz',
email: 'liz@example.com',
active: false
}
]
};
function reducer(state, action) {
switch (action.type) {
case 'CREATE_USER':
return produce(state, draft => {
draft.users.push(action.user);
})
case 'TOGGLE_USER':
return produce(state, draft => {
const user = draft.users.find(user => user.id === action.id);
user.active = !user.active;
})
case 'REMOVE_USER':
return produce(state, draft => {
const index = draft.users.findIndex(user => user.id === action.id);
draft.users.splice(index, 1);
})
default:
return state;
}
}
// UserDispatch 라는 이름으로 내보내줍니다.
export const UserDispatch = React.createContext(null);
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
const { users } = state;
const count = useMemo(() => countActiveUsers(users), [users]);
return (
<UserDispatch.Provider value={dispatch}>
<CreateUser />
<UserList users={users} />
<div>활성사용자 수 : {count}</div>
</UserDispatch.Provider>
);
}
export default App;

TOGGLE_USER 액션의 경우엔 확실히 immer를 사용하니 코드가 깔끔해졌지만 나머지의 경우엔 오히려 복잡해졌다.

상황에 따라 잘 선택해서 사용하면 된다. immer를 사용한다고 해서 모든 업데이트 로직에서 사용을 할 필요는 없다.

 

immer는 분명히 정말 편한 라이브러리인 것은 사실이다. 하지만, 확실히 알아두어야 하는 점은 성능적으로는 immer를 

사용하지 않은 코드가 조금 더 빠르다는 점이다. immer는 JavaScript 엔진의 Proxy라는 기능을 사용하는데, 구형 

브라우저 및 react-native 같은 환경에서는 지원되지 않으므로 (Proxy처럼 작동하지만 Proxy는 아닌) ES5 fallback을 사용하게 된다. ES5 fallback을 사용하게 되는 경우는 191ms 정도로, 꽤나 느려지게 된다. 물론 데이터가 별로 없다면 크게 

걱정할 필요는 없다.

 

immer라이브러리는 확실히 편하기 때문에, 데이터의 구조가 복잡해져서 불변성을 유지하면서 업데이트해야 하는데 

코드가 복잡해지는 상황이 온다면, 사용하는 것을 권장한다.

다만, 무조건 사용을 하진 말고 가능하면 데이터의 구조가 복잡해지게 되는 것을 방지하자. 그리고 어쩔 수 없을 때 immer를 사용하는 것이 좋다. immer를 사용한다고 해도, 필요한 곳에만 쓰고 간단히 처리될 수 있는 곳에서는 그냥 

일반 JavaScript로 구현하는 것이 좋을 것이다.

반응형