최근 Zustand에 관해 공부하고 사용해보는 경험을 갖었다.
예전부터 Zustand는 꼭 사용해보고 싶은 라이브러리였다.
npm trend를 보면 아래와 같은 통계를 볼 수 있다.
상태관리 라이브러리 npm 다운로드 횟수는
Redux : 675만
Zustand : 220만
jotai : 63만
recoil : 49만
인것을 볼 수 있다.
작년과 비교해보면
Redux : 675만 (43만 상승)
Zustand : 220만 (135만 상승)
jotai : 63만 (43만 상승)
recoil : 49만 (9만 상승)
jotai와 Redux는 43만 상승하였지만, Zustand는 약 3배인 135만 증가하였다.
(전체적으로 코딩 인구가 급증했다는 것도 보여주기도 한다.)
호기심이 안생길 수 없었다.
github Readme를 보면 Redux의 어떠한 단점을 커버하려고 했는지 잘적혀있다.
Why zustand over redux?
1. Simple and un-opinionated
2. Makes hooks the primary means of consuming state
3. Doesn't wrap your app in context providers
4. Can inform components transiently (without causing render)
1번과 2번은 Recoil에서도 가능하다.
주목해야할 점은 3번과 4번이다.
코드 설명은 위 장점을 중심으로 설명할 예정이다.
Why zustand over context?
Less boilerplate
Renders components only on changes
Centralized, action-based state management
다음으로는
왜 context를 사용하지 않는가?에 관한 이야기다.
context API는 React 상태관리에서 당연시 여겨졌지만, Zustand는 사용하지 않고 그러한 이유를 적었다.
보일러 플레이트를 줄일 수 있다. 👍
Context API를 사용하면 종종 Context.Provider와 Context.Consumer를 사용해야 하고, 별도의 로직을 작성해합니다. 이런 보일러플레이트(Boilerplate) 코드를 줄이기 위해 context API를 사용하지 않습니다.
변경 시에만 구성요소를 렌더링합니다. 👍
Context API는 상태 변경이 있을 때, 사용하는 모든 컴포넌트를 리렌더링합니다. Zustand는 Context API 사용하지 않아서 상태 변경에 따른 렌더링을 더 효율적으로 관리할 수 있습니다.
중앙 관리하는 액션 기반 상태관리 👍
모든 상태(데이터)를 한 군데에서 관리하고, 특정 '액션'을 실행해서 상태를 변경하는 방식으로 돌아간다.
Context API보다 더 단순하게 상태를 공유하고 바꿀 수 있어서, 코딩이 간단하다.
Zustand는 Redux나 Context API의 단점을 보완하면서, 더 단순하고 효율적인 상태 관리를 제공하려는 목적으로 설계하였다.
설치는 간단하게 npm이나 yarn을 이용한다.
npm install zustand
yarn add zustand
pnpm add zustand
이제 코드를 보면
장점 1. 심플하고 단순하다.
위의 장점을 볼 수 있다.
🕵️♀️ 스토어 생성
import { create } from 'zustand'
const useBearStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}))
create로 스토어를 생성하고, bears라는 상태를 추가하고,
increasePopulation, removeAllBears이라는 함수를 만들어서 관리할 수 있다.
store 안에서는 set을 사용하여 값을 업데이트 한다.
🤜 Zustand store 사용하기
장점 2. HOC -> HOOK으로
아까 만든 스토어를 import 하고, 상태를 선택하면 변경 사항에 따라 구성 요소가 다시 렌더링한다.
function BearCounter() {
const bears = useBearStore((state) => state.bears)
return <h1>{bears} around here ...</h1>
}
function Controls() {
const increasePopulation = useBearStore((state) => state.increasePopulation)
return <button onClick={increasePopulation}>one up</button>
}
bears와 increasePopulation는 아까 store 에서 만들었던 상태와 함수이다.
increasePopulation: () => set((state) => ({ bears: state.bears + 1 }))
increasePopulation를 사용하여서 값을 1 크게 만들면, bears는 업데이트한다.
이게 끝이다.
장점 3. 컨텍스 제공자를 쓰지 않는다.
적은 보일러플레이트와 context API를 사용하지 않고 Zustand를 사용할 수 있다.
자주 발생하는 상태 변경의 경우, 구독 기능을 사용하자.
장점 4. 렌더링 없이 상태 변화가 가능하다.
마지막 장점인 렌더링 없이 상태 변화가 가능하다.
코드는 마운트 해제 시를 고려해서 useEffect와 결합하는 것이 가장 좋다. 이는 뷰를 직접 변경할 수 있는 경우 성능에 큰 영향을 미칠 수 있습니다.
const useScratchStore = create((set) => ({ scratches: 0, ... }))
const Component = () => {
// Fetch initial state
const scratchRef = useRef(useScratchStore.getState().scratches)
useEffect(() => useScratchStore.subscribe(
state => (scratchRef.current = state.scratches)
), [])
Ref를 통해 값을 구독하고, 해당 값에 변화가 생기면 렌더링 없이 상태 변화가 가능하다.
직접 결과를 확인할 수 있게 codesandbox를 남기겠다.
카운터 버튼을 통해 렌더링 없이 값을 업데이트하는 것을 볼 수 있다.
useEffect(() => useScratchStore.subscribe(
state => (scratchRef.current = state.scratches)
), [])
위 로직을 부면 구독을 선언하는 동시에 return을 주고 있는데 이를 통해 언마운트 시에는 구독 해제를 하도록 세팅하고 있다.
zustand를 사용하면서 느꼈던 점은 store인 동시에 hook이라는 점이 편리하였다.
store 관련 함수를 한 곳에 모와서 관리할 수 있고, store에서 관리하니 동기화 걱정도 없었다.
라이브러리를 사용할 때, 해당 Readme나 guide를 보면 최고인 것 같지만, 항상 단점도 존재한다 그래서 단점을 찾아보고 있는데 아직 찾지를 못했다.
아무래도, context API 없는 것이 다른 상태관리에 비해 많이 유리하지 않을까 한다.
zustand를 사용하여서 성능을 개선해보고 싶다.
둘은 용도가 다르다고 생각합니다. Zustand는 전역 상태 관리에, Context는 지역 상태 관리에 적합하므로 두 가지를 병행하는 것이 적절하다고 봅니다. 예를 들어, Zustand로 리스트 UI의 각 아이템별 상태를 관리하고 액션을 실행해야 한다고 생각해보면, 상당히 복잡해질 수 있습니다. 하지만 Context는 각종 커스텀 훅에 접근이 용이 하고 위치에 상관없이 독립적으로 트리 내부에서만 유효하게 사용할 수 있으며, 언마운트 시 사용 중이던 데이터도 자동으로 메모리에서 정리된다는 장점이 있습니다. 둘은 같이 사용하는거지 하나를 완전히 버리고 가는것이 아닌 것 같습니다.