상태 관리는 견고한 React 애플리케이션을 만드는 데 가장 핵심적인 요소입니다. 서로 다른 컴포넌트들 간에 원활한 상태 공유, 업데이트 등은 특히 대규모 어플리케이션에서는 가장 신경써야할 부분이죠.
한동안은 Redux와 MobX 등이 React에서 상태를 처리하는 데 있어 가장 많이 사용되는 라이브러리였습니다. 그러나, 이들은 약간의 복잡성을 가지고 있고 보일러플레이트 코드 또한 양이 상당했는데요. 특히 작은 프로젝트나 단순성을 추구할 때는 적합하지 않은 라이브러리였습니다.
Zustand는 기존 라이브러리가 가지고 있는 복잡성과 상당한 코드량 문제 등을 해소하며 등장했습니다. Zustand는 단순하고 직관적인 API를 제공해 애플리케이션의 상태 관리를 쉽게 할 수 있도록 해줍니다. 이 글에서는 Zustand의 기본 컨셉과 핵심적인 기능들은 무엇인지 알아보고, 다른 라이브러리들과 비교했을 때 어떤 점이 장점이고 단점인지 파악해 보도록 하겠습니다.
독일어로 '상태'를 의미하는 Zustand는 React를 위한 작고 빠르며 확장 가능한 상태관리 라이브러리 입니다.
전통적인 상태 관리 라이브러리와 달리 Zustand는 스토어 중심 접근 방식으로 프로세스를 단순화했고, React 함수형 컴포넌트에서 context와 hooks를 활용해 추가적인 의존성이나 보일러플레이트 코드 없이도 애플리케이션의 상태를 관리할 수 있습니다.
단순하고 효율적이며 사용하기 쉽게 설계되어 있어, Redux나 MobX와 같은 복잡한 상태 관리 솔루션을 피하고 경량화된 대안을 찾는 개발자들에게 좋은 선택 사항입니다.
import { create } from 'zustand';
interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
}
export const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
Zustand의 애플리케이션 상태의 single source of truth인 스토어는 애플리케이션의 상태를 관리하는 장소입니다. 이 스토어는 Zustand가 제공하는 create 함수를 사용해 생성됩니다.
Zustand의 상태는 애플리케이션이 관리하고자 하는 데이터를 의미합니다. 시간이 지남에 따라 변할 수 있는 정보로 객체, 배열, 원시 타입 등 어떤 타입이든 될 수 있습니다.
Zustand의 액션은 상태를 수정할 수 있게 해주는 함수들입니다. 이 함수들은 스토어 내에서 정의되며 제어된 방식으로 상태를 업데이트하는 역할을 합니다. 위의 예시에서 increment와 decrement는 count 상태를 변경하는 액션입니다. Redux의 리듀서와 비슷하지만 더 유연하고 정의하기 쉽습니다.
Zustand는 React 컴포넌트 내에서 상태와 액션에 접근할 수 있는 훅을 제공합니다. 이 훅은 일반적으로 useStore(또는 스토어를 생성할 때 지정한 이름)입니다. 이 훅을 사용해 컴포넌트에서 상태값에 접근하고 업데이트 할 수 있습니다.
import React from 'react';
import { useCounterStore } from './counterStore';
const Counter: React.FC = () => {
const { count, increment, decrement } = useCounterStore();
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
};
export default Counter;
Zustand는 컴포넌트 리렌더링을 효율적으로 처리합니다. useStore 훅을 사용하면, Zustand는 자동으로 이 컴포넌트를 구독처리합니다. 이렇게 구독 처리를 해두면, 관련한 상태값이 변경될 때만 컴포넌트가 리렌더링되고 불필요한 리렌더링이 줄어들며 성능을 향상시킬 수 있습니다.
Zustand는 Typescript와 잘 작동합니다. 상태와 액션에 대한 타입을 정의하여 애플리케이션 전체의 타입 안정성을 보장하고, 런타임 에러를 줄일 수 있습니다.
Zustand는 스토어에 미들웨어를 추가할 수 있게 해주며, 이는 디버깅, 로깅, 또는 컴포넌트로 전달되기 전 상태 변화를 중간에서 가로채는 필요한 작업을 할 수 있도록 도와줍니다.
const logger = (config, prevState, nextState) => {
console.log('State changed:', prevState, '->', nextState);
return config;
};
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}), { middleware: [logger] });
여러 Zustand 스토어를 생성하고 필요에 따라 구성할 수 있도록 해 상태관리를 깔끔하게 체계적인 방법으로 할 수 있습니다.
앞서 언급했던 Zustand의 핵심 기능들은 아래와 같이 정리할 수 있습니다.
- 단순성: 최소한의 API를 가지고 있으며 배우고 사용하기 쉽습니다.
- 보일러플레이트가 없음: 다른 상태 관리 라이브러리와 달리, Zustand는 매우 적은 설정 코드만을 필요로 합니다.
- 훅 기반: Zustand는 React hooks를 활용하여 React 개발에 자연스럽게 어울립니다.
- Typescript 지원: Typescript와 잘 작동하며, 뛰어난 타입 추론을 제공합니다.
- 미들웨어 지원: Zustand는 미들웨어를 통해 기능을 확장할 수 있게 해줍니다.
- 개발자 도구 지원: Redux DevTools와 통합되어 Time-travel debugging 등 강력한 디버깅을 지원합니다. 이에 더해 상태 변화를 실시간으로 모니터링할 수 있고, 상태 액션 히스토리를 확인할 수 있습니다.
- 확장성: 주로 React와 함께 사용되지만, Zustand는 모든 Javascript 환경에서 사용될 수 있습니다.
Zustand는 상태 관리에 있어 독특한 접근 방식을 취하며, Redux, Valtio, Jotai, Recoil과 같은 라이브러리들과 차별화됩니다. 그렇지만, 완벽한 도구는 없기 때문에 각 라이브러리가 어떤 기능들을 제공하는지 살피고 프로젝트에 맞는 라이브러리를 선택하는 것이 중요하겠죠.
Zustand 공식 홈페이지에서는 Redux, Valtio 등 라이브러리와 Zustand를 비교하는 글이 있습니다. 이러한 라이브러리들과 어떻게 비교되는지 그리고 그것의 장점과 단점은 무엇인지 살펴보겠습니다.
Zustand와 Redux 모두 불변 상태 모델을 따르지만, Redux는 광점위한 보일러플레이트 코드를 수반하고 컨텍스트 래핑이 필요한 반면, Zustand는 가벼우며 React의 컨텍스트 시스템에 의존하지 않습니다.
// Redux
import { createStore } from 'redux';
const reducer = (state = { count: 0 }, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + action.payload };
default:
return state;
}
};
const store = createStore(reducer);
Zustand
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
두 라이브러리 모두 React 애플리케이션에서 전역 상태를 관리하고 업데이트할 수 있게 해주며, 훅을 통해 상태에 접근할 수 있도록 해줍니다. Zustand는 Redux보다는 직관적인 라이브러리로, 상태를 읽고 업데이트 하기 위한 훅과 상태 업데이트를 수행하기 위한 액션을 포함하여 최소한의 코드로 상태관리 및 업데이트를 가능하게 합니다. 다만, Redux는 대규모 프로젝트에 적합한 라이브러리로, 미들웨어와 비동기 액션 지원 등이 탁월합니다. 또한 Zustand와 비교해 광범위한 커뮤니티와 생태계를 제공하고 있습니다.
Zustand는 불변 상태 모델을 사용하는 반면, Valtio는 프록시를 사용한 가변적 접근 방식을 취해 상태 속성을 직접 업데이트 할 수 있게 해줍니다.
// Valtio
import { proxy } from 'valtio';
const state = proxy({ count: 0 });
state.count += 1;
// Zustand
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
Zustand는 명시적인 상태 업데이트로 디버깅이 더 예측 가능하다는 장점이 있고, 선택적 구독으로 불필요한 렌더링이 감소된다는 장점이 있습니다. 반면 Valtio는 프록시를 사용해 함수형 프로그래밍에 익숙하지 않은 개발자들도 직관적으로 상태를 관리할 수 있도록 하고, 컴포넌트가 접근한 속성을 기반으로 자동으로 리렌더링되어 반응성을 단순화 했다는 장점을 가지고 있습니다.
Jotai는 상태를 더 작고 독립적인 Atom으로 분리하는 반면, Zustand는 단일 중앙집중식 스토어로 운영됩니다.
// Jotai
import { atom, useAtom } from 'jotai';
const countAtom = atom(0);
function Component() {
const [count, setCount] = useAtom(countAtom);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// Zustand
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
function Component() {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
return <button onClick={increment}>{count}</button>;
}
Zustand는 여기저기에서 사용되는 상태값을 가진 애플리케이션에서 중앙집중식으로 상태를 관리해 상태관리가 더 쉽습니다. 또한 atom 의존성과 같은 개념을 도입하지 않고도 배우고 사용하기 더 간단합니다. 반면 Jotai는 atom을 통해 상태를 분해하고 재구성하는 데 더 큰 유연성을 제공하고, 의존성 기반으로 컴포넌트가 리랜더링 되도록 보장함으로써 자동 렌더링이 최적화되어 있습니다.
Recoil은 컨텍스트 프로바이더가 필요하고 상태 의존성을 관리하기 위해 atom 문자열 키를 사용하는 반면, zustand는 react 내부 구조와 분리되어 있으며 앱을 래핑할 필요가 없습니다.
// recoil
import { atom, useRecoilState } from 'recoil';
const countAtom = atom({
key: 'count',
default: 0,
});
function Component() {
const [count, setCount] = useRecoilState(countAtom);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// Zustand
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
function Component() {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
return <button onClick={increment}>{count}</button>;
}
Zustand는 앱 전체 컨텍스트 래퍼가 필요 없고, 간단한 상태 관리를 위한 오버헤드가 적은 편입니다. 반면 Recoil은 상태 의존성 그래프를 통해 atom간의 관계를 추적할 수 있다는 장점이 있습니다.
Zustand를 사용하기 위해 프로젝트 root에서 아래와 같이 실행합니다.
npm install zustand
# or
yarn add zustand
import { create } from 'zustand'
const useStore = create((set) => ({
// 상태
count: 0,
// 액션
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}))
import React from 'react'
function Counter() {
// 전체 상태 사용
const { count, increment, decrement, reset } = useStore()
// 또는 특정 상태만 선택
const count = useStore((state) => state.count)
const increment = useStore((state) => state.increment)
return (
<div>
<h1>{count}</h1>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>Reset</button>
</div>
)
}
const useStore = create((set) => ({
user: null,
fetchUser: async (id) => {
const response = await fetch(`/api/user/${id}`)
const user = await response.json()
set({ user })
}
}))
import { persist } from 'zustand/middleware'
const useStore = create(
persist(
(set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
}),
{
name: 'bear-storage', // 스토리지 키
}
)
)
interface BearState {
bears: number
increase: (by: number) => void
}
const useStore = create<BearState>((set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
}))
const useStore = create((set) => ({
firstName: 'John',
lastName: 'Doe',
updateNames: (firstName, lastName) =>
set(() => ({ firstName, lastName })),
}))
const unsub = useStore.subscribe(
(state, prevState) => console.log('상태 변경:', state, prevState)
)
// 구독 해제
unsub()
import { devtools } from 'zustand/middleware'
const useStore = create(devtools((set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
})))
// userStore.js
import { create } from 'zustand'
const useUserStore = create((set) => ({
user: null,
isLoading: false,
error: null,
login: async (credentials) => {
set({ isLoading: true, error: null })
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
})
const user = await response.json()
set({ user, isLoading: false })
} catch (error) {
set({ error: error.message, isLoading: false })
}
},
logout: () => set({ user: null })
}))
// UserComponent.jsx
function UserProfile() {
const { user, login, logout, isLoading, error } = useUserStore()
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
return user ? (
<div>
<h2>Welcome, {user.name}!</h2>
<button onClick={logout}>Logout</button>
</div>
) : (
<button onClick={() => login(credentials)}>Login</button>
)
}
Zustand는 최소한의 보일러플레이트와 스토어 중심 모델로 상태 관리를 단순화 합니다. 단순성과 React통합에서 뛰어나지만, Redux의 미들웨어, Valtio의 가변 상태, Jotai의 세분화된 atom, 또는 Recoil의 의존성 추적과 같은 기능들은 부족한 상태입니다.
어떤 상태관리 라이브러리도 완벽하지 않습니다. 가볍고 효율적인 상태관리를 원한다면 zustand를 선택하되, 앞서 열거한 다른 것들이 필요한 경우 대안을 고려하는 것이 좋다고 생각합니다.
https://zustand.docs.pmnd.rs/getting-started/introduction
https://www.pedroalonso.net/blog/react-state-management-zustand/
https://dev.to/ricardogesteves/zustand-when-how-and-why-1kpi
https://medium.com/@ansi_86801/mastering-zustand-a-comprehensive-guide-7c7db8b717bd
https://codeparrot.ai/blogs/zustand-key-features-state-management-simplified
https://dev.to/avt/understanding-zustand-a-beginners-guide-with-typescript-4jjo
https://medium.com/@olayidecodes/zustand-a-simplified-state-management-for-react-1071bde2f0d3
https://json-5.com/awesome-react-state-management