React를 사용하면서 필수적으로 함께 생각해야하는것은 state management library이다. 최근들어 다양한 선택지들이 존재해졌지만 한때는 당연히 Redux를 사용해서 상태관리를 했던때가 있었다. 하지만 요즘의 흐름은 hook이 등장하면서 모든것이 바뀌었다고 해도 무방하다. 기존의 솔루션이 훨씬 사용하기 쉽게 변화했고 새로운 솔루션이 탄생하기도 했다.
그 중 하나가 오늘 소개할 Zustand이다. Zustand는 global state를 저장하고 select하는 간단한 api를 제공하는 library이다. 가볍고 쉽게 사용할 수 있는 state manager 라고 할수 있겠다. (개인적으로는 useReducer를 뚝 떼어논 형태라고 생각한다)
Zustand는 다양한 library와 결합해서 사용할 수 있다. immutable한 변화를 쉽게 컨트롤하기 위해 immer와 함께 사용할 수도 있고, 다양한 middleware를 제공해주어 편의를 제공한다. 다음은 zustand의 몇가지 장점이다.
- 특정
library에 엮이지 않고 다양한library를 결합해 사용할 수 있다. - 하나의 중앙
store를 만들어 사용하면서,state를 정의/사용하는것이 단순하다. - 동작을 이해하기 위해 알아야 하는 코드량이 매우 적다.
rendering optimization을 하기가 쉽다<Provider />로 감싸주지 않고 독립적으로 사용이 가능하다
How to use the Zustand?
store를 만들때는 create 함수를 이용하여 state와 action을 정희할 수 있다. 그러면 hook을 return하는 형태이다.
import { create } from "zustand";
export const useBookStore = create(
(set) => ({
amount: 30,
title: "Alice wonderland",
actions: {
addAmount: (value: number) =>
set((state) => ({ amount: state.amount + value })),
},
})
);component단에서 state를 꺼내기 위해서는 selector를 전달해주어야한다. selector를 명시하지 않으면 store 전체를 return 하게된다.
import useBookStore from './bookStore';
const BookShelf = () => {
const amount = useBookStore(state => state.amount);
const { addAmount } = useBookActions(state => state.actions);
return (
<>
<h1>{amount}</h1>
<button onClick={() => addAmount(10)}>Change the amount</button>
</>
)
};selector에서 object를 반환하려는 경우, shallow compare를 사용하여 re-render를 줄이도록 option을 넣어줄 수 있다 (최적화 기능)
import { shallow } from 'zustand/shallow';
// only re-renders when either amount or title changes
const { amount, title } = useBookStore(
(state) => ({ amount: state.amount, title: state.title }),
shallow
);다양한 middleware를 손쉽게 적용할 수 있다. 특히 react와는 뗄수없는 immer, devtool을 사용하는 방법은 다음과 같이 간단하다. (local storage, session storage를 지원하는 middleware도 손쉽게 사용가능하다)
import { immer } from "zustand/middleware/immer"; // npm install immer 필요
import { devtools } from "zustand/middleware";
const useBookStore = create(
devtools( // devtool 적용
immer((set) => ({ // immer 적용
amount: 30,
title: "Alice wonderland",
actions: {
addAmount: (value: number) =>
set((state) => ({ amount: state.amount + value })),
subAmount: (value: number) =>
set((state) => ({ amount: state.amount - value })),
changeTitle: (value: string) =>
set((state) => ({ title: state.title + value })),
},
}))
)
);Zustand는 Context API 사용을 지양하고 closure를 활용하여 store 내부상태를 관리한다. (closure는 간단히 말해서 함수가 선언될 당시의 주변 환경을 기억하는 것이다 참고). 따라서 zustand store hook을 호출하여 리턴된 useHook을 어느 컴포넌트에서 사용하더라도 같은 store를 바라보게 된다.
store를 생성하는 함수를 호출할때 closure를 활용하기 때문에 상태의 변경, 조회, 구독등의 interface를 통해서 store를 다루고 실제 상태는 의도치 않게 변경되는 것을 막을 수 있다.
마치며
현재까지는 Redux를 압도적으로 많이 사용하고 있고, Redux를 제외한 상태관리 라이브러리는 비슷비슷한 점유율을 보이고 있다. 하지만 점점 사용이 쉽고 코드량이 간소화 되는 추세로 변화하고 있는것 같기는 하다. 간단하고 편리하지만 다양한 옵션과 이점이 있기 때문에 좀 더 공부해봐야 겠다.
- https://blog.axlight.com/posts/steps-to-develop-global-state-for-react/
- https://github.com/pmndrs/zustand
- https://ui.toast.com/posts/ko_20210812
- https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures