Zustand와 Emotion을 활용하여 간단한 투두리스트를 구현하던 중, 할 일 체크 기능을 구현하는 과정에서 Immer를 활용해봤다.
디자인은 일단 뒷전...
// checkTodo 예시
checkTodo: (id) =>
set((state) => ({
todo_list: state.todo_list.map((todo) =>
todo.id === id ? { ...todo, isChecked: !todo.isChecked } : todo
),
})),
이렇게 일일히 map 돌면서 id를 찾고, 거기에 이제 스프레드연산자 활용해서 체크기능 구현했는데, 코드가 너무 번잡하게 느껴짐
1. Immer
2. optics-ts
3. Ramda
이렇게 복잡한 구조에 대한 솔루션을 3가지 추천하고 있었.
그 중 그나마 들어봤고, 간단해보이는 Immer에 대해서 알아봤다.
상태를 변경하는데 불변성을 유지하는 코드?라는것은 이게 바꾸고자하는 상태를 제외한 모든 상태를 바뀌지않게 묶어두면서 변경하는 코드 라는 뜻을 의미하는 듯 하다
공식문서의 극단적인 예시를 보면 deep.nested.obj..
normalInc: () =>
set((state) => ({
deep: {
...state.deep,
nested: {
...state.deep.nested,
obj: {
...state.deep.nested.obj,
count: state.deep.nested.obj.count + 1
}
}
}
}))
...
)를 사용하여 불변성을 유지하는데, 이걸 일일히 파고 들어가니까 코드가 길어지고 번잡해진다.immerInc: () =>
set(produce((state: State) => { ++state.deep.nested.obj.count })),
이렇게 코드길이와 가독성 면에서 더 눈에 띄는 개선이 이루어진 것을 확인할 수 있다.
//1안 -> Without Immer
const onToggle = id => {
setUsers(
users.map(user =>
user.id === id ? { ...user, active: !user.active } : user
)
);
};
//2안 -> With Immer
setState(state => {...state, key: newValue})
딱봐도 알 수 있듯이 한눈에도 코드가 줄은 것이 확인이 가능했다. 약간 lodash처럼 좀 더 쉽게 원하는 기능을 구성하게끔 메소드를 제공하는 라이브러리 정도라고 이해할 수 있었다.
그래서 이제 내 코드에 적용을 하자면…
checkTodo: (id) =>
set((state) => ({
todo_list: state.todo_list.map((todo) =>
todo.id === id ? { ...todo, isChecked: !todo.isChecked } : todo
),
})),
checkTodo: (id) => set(
produce((state) => {
const todo = state.todo_list.find(todo => todo.id === id);
if (todo) {
todo.isChecked = !todo.isChecked;
}
})
),
setNewTodo
)의 경우 Immer를 사용할 필요 없음지금은 depth가 한단계 뿐이 없어서 사실 map을 돌려도 큰 문제는 없는데, 이후에 더 깊은 상태관리에서는 코드의 안정성이나 가독성이 너무 좋은 것 같아서 자주 사용할 예정이다.