리액트에서는 배열이나 객체를 업데이트 해야 할 때에는 직접 수정하면 안되고 불변성을 지켜주면서 업데이트를 해야함
예를 들어
// 잘못된 코드
const object = {
a: 1,
b: 2
};
object.b = 3;
console.log(object);
이렇게 하면 안됌
이렇게 해야함
spread 연산자 ... 를 사용해서 새로운 객체를 생성
// 이렇게 사용 (불변성을 지켜주어야함)
const object = {
a: 1,
b: 2
};
const nextObject = {
...object,
b: 3
};
console.log(nextObject);
배열도 마찬가지임
push, slice 등의 함수를 사용하거나 n 번째 항목을 직접 수정하면 안되고
concat, filter, map 등의 함수를 사용해야함
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
);
console.log(todos);
console.log(inserted);
console.log(filtered);
console.log(toggled);


간단한 데이터야 spread 함수를 사용해서 불변성을 지켜갈수 있다고 하지만
데이터의 구조가 조금 까다로워 진다면? ...
아래의 객체를 보자
const state = {
posts: [
{
id: 1,
title: '제목이에용',
body: '내용이에용',
comments: [
{
id: 1,
text: '너무 복잡해지는걸...?'
}
]
},
{
id: 2,
title: '두번째 제목이에용',
body: '두번쨰 내용이에용',
comments: [
{
id: 2,
text: '휴.. 벌써 어지럽다..'
}
]
}
],
selectedId: 1
};
... 살짝 어지럽긴 한데
여기서 posts 배열안의 id 가 1인 post 객체를 찾아서, comments 에 새로운 댓글 객체를 추가해줘야 한다고 가정해보자
const state = {
posts: [
{
id: 1,
title: '제목이에용',
body: '내용이에용',
comments: [
{
id: 1,
text: '너무 복잡해지는걸...?'
}
]
},
{
id: 2,
title: '두번째 제목이에용',
body: '두번쨰 내용이에용',
comments: [
{
id: 2,
text: '휴.. 벌써 어지럽다..'
}
]
}
],
selectedId: 1
};
const nextState = {
...state,
posts: state.posts.map(post =>
post.id === 1
? {
...post,
comments: post.comments.concat({
id: 3,
text: '반가워요 새로운 댓글이에용'
})
}
: post
)
};
console.log(state);
console.log(nextState);


이런식으로 코드를 짜야함 근데 조금 번잡스러운 느낌이 있음 코드가 한눈에 들어오지 않는 느낌도 있어서
immer 라이브러리를 사용해보면 아래와 같은 코드가 된다
const nextState = produce(state, draft => {
const post = draft.posts.find(post => post.id === 1);
post.comments.push({
id: 3,
text: '이게되나?'
});
});

코드가 조금 깔끔해졌음
Immer 라이브러리를 사용하면 상태 업데이트를 할 때 불변성을 신경쓰지 않으면서 업데이트를 해줘도 된다
immer 이 친구가 대신 해줄거임ㅋㅋ 그럼 사용해보자
Immer 사용법
** 들어가기 전 참고 : 이 라이브러리를 권장하는것은 아님. 안쓰는게 더 좋을수도 있음
해당 프로젝트 터미널에 immer 를 추가해주자
yarn 도 가능
$ npm add immer
그리고 코드에서 import 를 시켜주자
보통 produce 라는 이름으로 불러옴
import produce from "immer";
produce 함수를 사용 할때에는 첫번째 파라미터에는 수정하고 싶은 상태,
두번째 파라미터는 어떻게 업데이트하고 싶을지 정의하는 함수를 넣어주면 된다.
두번째 파라미터에 넣은 함수에서는 불변성에 대해서 신경 1도 쓰지말고 업데이트 해주자 알아서 다 해줌ㅋ
import produce from "immer";
const state = {
number: 1,
dontChangeMe: 2
};
const nextState = produce(state, (draft) => {
draft.number += 5;
});
console.log(nextState);

그럼 App.js 에도 적용해 봐야겠지?
import React, {useMemo, useReducer} from 'react';
import CreateUser from './components/CreateUser';
import UserList from "./components/UserList";
import produce from "immer";
function countActiveUsers(users) {
console.log("active 값이 ture 인 유저의 수를 센는중...");
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
}
]
};
// 불변성을 지켜주기 위해서 spread 연산자를 사용
// 여기서 recuder 로 기능 구현
function reducer(state, action) {
switch (action.type) {
case 'CREATE_USER' :
return produce(state, draft => {
draft.users.push(action.user);
})
// return {
// inputs: initialState.inputs,
// users: state.users.concat(action.user)
// };
case 'TOGGLE_USER' :
return produce(state, draft => {
const user = draft.users.find(user => user.id === action.id);
user.active = !user.active;
});
// return {
// ...state,
// users: state.users.map(user =>
// user.id === action.id ? {...user, active: !user.active } : user
// )
// };
case 'REMOVE_USER' :
return produce(state, draft => {
const index = draft.users.findIndex(user => user.id === action.id);
draft.users.splice(index, 1);
});
// return {
// ...state,
// users: state.users.filter(user => user.id !== action.id)
// };
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 에 props 를 전달하는 곳임 */}
<UserList users={users} key={users.id}/>
<div>활성 사용자 수 : {count}</div>
</UserDispatch.Provider>
);
}
export default App;
주석은 약간 비교해 보려고 남겨둔것
Immer 라이브러리를 사용해서 간단해지는 업데이트가 있고 오히려 코드가 길어지는 업데이트가 있음
TOGGLE_USER 액션의 경우에는 코드가 깔끔해졌지만 나머지는 오히려 복잡해 진 경우가 되었다...
** 상황에 따라 잘 선택해서 쓰자 > 객체가 깊은곳에 있을 땐 immer 아니면 concat , filter 를 사용하자
Immer 와 함수형 업데이트
const [todo, setTodo] = useState({
text: 'Hello',
done: false
});
const onClick = useCallback(() => {
setTodo(todo => ({
...todo,
done: !todo.done
}));
}, []);
이렇게 setTodo 함수에 업데이트를 해주는 함수를 넣어서 useCallback 을 사용하는 경우
두번째 파라미터인 deps 배열에 todo 를 넣지 않아도 됨
만약에 produce 함수에 두개의 파라미터를 넣게 된다면 첫번째 파라미터에 넣은 상태를 불변성을 유지하면서 새로운 상태를 만들어주지만
첫번째 파라미터를 생략하고 바로 업데이트 함수를 넣어주면 반환 값은 새로운 상태가 아닌 상태를 업데이트 해주는 함수가 된다.
const todo = {
text: 'Hello',
done: false
};
const updater = produce(draft => {
draft.done = !draft.done;
});
const nextTodo = updater(todo);
console.log(nextTodo);
// { text: 'Hello', done: true }
produce 가 반환하는것이 업데이트 함수가 되기 때문에 useState 의 업데이트 함수를 사용할 때 아래와 같이 구현가능
const [todo, setTodo] = useState({
text: 'Hello',
done: false
});
const onClick = useCallback(() => {
setTodo(
produce(draft => {
draft.done = !draft.done;
})
);
}, []);
** immer 은 불변성을 알아서 처리하기 때문에 편하긴 하나 기본 코드 spread 를 사용한 코드가 훨씬 빠르다..
데이터가 50000개 원소중에서 5000개 원소를 업데이트 한 결과 속도의 차이는
31ms (immer) vs 6ms (기본) .......
** 또 immer 은 js 엔진의 Proxy 기능을 사용하는데 구형 브라우저 및 react-native 에서는 지원이 안된다는거 같다
(proxy 처럼 작동하지만 proxy 가 아님) ES5 fallback 을 사용함 >> 드럽게 느림..
결론 . 무조건 immer 를 사용하지말고 어쩔수 없을 때 사용하자 ....
'JS Library > React' 카테고리의 다른 글
| Sass (0) | 2022.08.01 |
|---|---|
| useReducer 요청 상태 관리 (0) | 2022.06.09 |
| API 연동하기, axios (0) | 2022.06.09 |
| Context API 전역 값 관리 (0) | 2022.05.26 |
| 커스텀 Hooks (0) | 2022.05.25 |
| React.memo (0) | 2022.05.25 |