React에서 불변성을 유지하는 코드를 작성하기 쉽게 해주는 라이브러리
불변성은 메모리 주소를 변경할 수 없는 것을 의미한다.
참고 🌐
Javascript 객체 기본 메소드 map, filter, concat, slice 등을 사용하여 불변성을 지키며 상태를 업데이트할 수 있다.
배열에 추가
setUsers(state.array.concat(user));
배열에서 삭제
const onRemove = (id) => {
// user.id 가 id인 것을 제거
setUsers(users.filter((user) => user.id !== id));
};
배열에서 수정
const onToggle = (id) => {
setUsers(
users.map((user) =>
user.id === id ? { ...user, active: !user.active } : user
)
);
};
객체에서 추가
setState(state => {...state, key: value})
위 방법대로 불변성을 지키며 상태를 업데이트하면 코드가 길어지고 복잡해진다. 이때 immer.js를 사용하면 일반 객체나 배열을 다루듯 불변성을 유지하면서 상태를 업데이트 할 수 있다.
const baseState = [
{
title: "Learn TypeScript",
done: true,
},
{
title: "Try Immer",
done: false,
},
];
공식 홈페이지의 예를 가져왔다 baseState 상태를 변경해보자.
const nextState = baseState.slice(); // 새로운 배열 생성
nextState[1] = {
// 첫 번째 요소 교체
...nextState[1], // 기존 1번째 요소 복사
done: true, // 변화를 주고 싶은 값만 덮어씌우기
};
// nextState는 새로운 배열이기 때문에 push를 사용해도 안전하다.
// 임의의 시간에 미래에 동일한 작업을 수행하면
// 불변성 원칙을 위반하고 버그를 발생시킨다.
nextState.push({ title: "Tweet about it" });
import produce from "immer";
const nextState = produce(baseState, (draft) => {
draft[1].done = true;
draft.push({ title: "Tweet about it" });
});
immer에서 불변성 유지를 이용해 사용할 produce 함수의 첫 번째 인자는 수정하고 싶은 객체, 배열이고 두 번째 인자는 첫 번째 인자를 어떻게 업데이트할지 정의하는 recipe 함수이다.
공식 홈페이지에서는 이를 아래 사진과 같이 설명하고 있다.

immer를 사용하는 것은 개인 비서를 가지는 것과 같다.
비서가 letter (현재 상태)를 받고) 변경 사항을 기록할 사본(draft)을 제공한다. 초안을 작성하면 비서가 당신의 초안을 가지고 당신을 위해 진짜 불변의 letter를 작성한다.
import React, { useCallback, useState } from "react";
import produce from "immer";
const TodoList = () => {
const [todos, setTodos] = useState([
{
id: "React",
title: "Learn React",
done: true
},
{
id: "Immer",
title: "Try Immer",
done: false
}
]);
const handleToggle = useCallback((id) => {
setTodos(
produce((draft) => {
const todo = draft.find((todo) => todo.id === id);
todo.done = !todo.done;
})
);
}, []);
const handleAdd = useCallback(() => {
setTodos(
produce((draft) => {
draft.push({
id: "todo_" + Math.random(),
title: "A new todo",
done: false
});
})
);
}, []);
return (<div>{*/ See CodeSandbox */}</div>)
}
setState 함수안에 produce 함수를 넣어주고 기존 state를 draft로 받아서 사용하면 된다.
더 간단하게 사용하기 위해 immer.js에서 제공하는 useImmer 훅을 사용해보자.
NPM use-immer
import React, { useCallback } from "react";
import { useImmer } from "use-immer";
const TodoList = () => {
const [todos, setTodos] = useImmer([
{
id: "React",
title: "Learn React",
done: true
},
{
id: "Immer",
title: "Try Immer",
done: false
}
]);
const handleToggle = useCallback((id) => {
setTodos((draft) => {
const todo = draft.find((todo) => todo.id === id);
todo.done = !todo.done;
});
}, []);
const handleAdd = useCallback(() => {
setTodos((draft) => {
draft.push({
id: "todo_" + Math.random(),
title: "A new todo",
done: false
});
});
}, []);
// etc
위와 같이 useState 대신 useImmer를 사용해서 불변성을 유지하면서 상태를 간단하게 업데이트 할 수 있다.
불변성에 관해 공부하고 immer.js를 사용하는 목적과 사용법도 알아보았는데 사실 크게 와닿지 않아서 immer.js를 사용해야 하는 상황을 찾아보았다.
const [state, setState] = useState({
a: {
b: {
c: 1,
},
},
});
위와 같은 상태가 있다고 가정하고 불변성을 유지하며 상태를 변경하려면 다음과 같이 작성해야 한다.
setState({
...state,
a: {
...state.a,
b: {
...state.a.b,
c: 2,
},
},
});
immer.js를 사용하면 다음과 같이 작성할 수 있다.
import produce from "immer";
setState(
produce(state, (draft) => {
draft.a.b.c = 2;
})
);
useImmer 훅을 사용하는 경우
// useImmer 훅 사용해서 state 초기화
const [state, setState] = useImmer({
a: {
b: {
c: 1,
},
},
});
const handleClick = () => {
setState((draft) => {
draft.a.b.c = 2;
});
};