
Zustand란 무엇인가?
Zustand(독일어로 "상태"를 의미)는 React의 상태 관리 문제를 해결하기 위해 등장한 단순하면서도 강력한 상태 관리 라이브러리다. Redux처럼 불필요한 보일러플레이트 없이 직관적인 API를 제공하며, Flux기반이지만 Flux 아키텍처의 제약에서 벗어나 간결한 코드 작성을 가능하게 한다.
Zustand의 철학
최소한의 코드 (Minimalism)
- Zustand는 상태 관리 라이브러리 중에서도 가장 적은 보일러플레이트를 요구한다.
- create 함수를 사용해 간단하게 스토어를 생성할 수 있다.
- useStore 같은 Hook을 통해 원하는 데이터를 구독할 수 있다.
- Redux처럼 Reducer, Action, Middleware 등의 복잡한 개념 없이도 순수 함수 기반의 상태 관리가 가능하다.
즉, 불필요한 개념을 제거하고 필요한 기능만 제공하는 것을 목표로 한다.
특정한 구조를 강제하지 않음 (Unopinionated)
- Redux는 action, reducer, store 등 정해진 구조를 강제하지만,
- Zustand는 특정한 설계를 따를 필요가 없다.
- Flux 패턴을 따를 필요 없이, 원하는 방식으로 스토어를 구성할 수 있다.
- 동기/비동기 상태 업데이트가 자유롭다.
즉, 개발자가 원하는 방식대로 상태를 관리할 수 있도록 유연성을 제공하는 것을 목표로 한다.
빠르고 확장 가능한 구조 (Fast & Scalable)
- Zustand는 내부적으로 React의 useState와 유사한 개념을 사용하면서도 더 나은 성능을 보장한다.
- React Context를 사용하지 않으므로 리렌더링 최적화가 쉽다.
- 비동기 데이터 관리를 간편하게 할 수 있다.
- 상태 변경 시 불필요한 리렌더링을 방지하기 위해 셀렉터 패턴을 지원한다.
즉, React의 성능 문제를 해결하면서도 간단한 사용성을 유지하는 것을 목표로 한다.
Zustand의 개념
Zustand는 store(스토어) 개념을 기반으로 동작하며,
이는 React의 useState를 확장한 형태로 볼 수 있다.
import { create } from 'zustand';
// 스토어 생성
const useStore = create((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
decrease: () => set((state) => ({ count: state.count - 1 })),
}));
export default useStore;
스토어를 만든 후, 컴포넌트에서 간편하게 상태를 사용할 수 있다.
import useStore from './store';
function Counter() {
const { count, increase, decrease } = useStore();
return (
<div><p>Count: {count}</p><button onClick={increase}>+</button><button onClick={decrease}>-</button></div>);
}
📌 Redux와 비교하면?
Redux에서는 useSelector, useDispatch를 사용해야 하지만, Zustand는 단순한 Hook을 사용한다.
Redux에서는 dispatch({ type: "INCREMENT" }) 같은 보일러플레이트 코드가 필요하지만,Zustand는 간단한 함수를 호출하면 된다.
Zustand는 Redux와 달리 불변성을 강제하지 않는다.
즉, 일부 상태만 변경할 수 있으며, 전체 상태를 다시 선언할 필요가 없다.
📌 Redux에서는 immer 같은 라이브러리를 사용해야 하지만, Zustand는 기본적으로 이를 지원한다.
Zustand는 로깅, 퍼시스턴스(영구 저장), Immer 사용 등의 기능을 미들웨어로 확장할 수 있다.
useMemo 또는 React.memo 등을 활용해야 하지만, 설정이 복잡해진다.Action, Reducer, Dispatch, Store 등 Flux 아키텍처 기반의 개념을 따르다 보니 구조가 복잡하다.useState는 컴포넌트 내부에서만 상태를 관리할 수 있다.useReducer를 사용하면 전역 상태를 관리할 수 있지만, Context를 함께 사용해야 하는 불편함이 있다.useReducer나 useContext를 사용하면, 값이 변경될 때마다 관련된 모든 컴포넌트가 리렌더링된다.// 1. 상태와 리스너 관리
let state; // 이 변수는 "현재 상태"를 저장해요. 클로저
const listeners = new Set(); // 이곳에는 상태가 바뀔 때 호출될 함수들(listener)이 저장돼요.
// 2. setState: 상태를 업데이트하는 함수
const setState = (partial, replace) => {
// partial: 새로운 상태 정보(객체) 또는 이전 상태를 기반으로 만들어진 결과를 반환하는 함수
const nextState = typeof partial === 'function' ? partial(state) : partial;
// 현재 상태(state)와 새 상태(nextState)가 다를 때만 업데이트
if (!Object.is(nextState, state)) {
const previousState = state;
// replace 플래그가 있거나, nextState가 객체가 아닌 경우엔 그대로 대체
// 아니면 기존 state와 nextState를 병합(merge)해요.
state =
replace ?? (typeof nextState !== 'object' || nextState === null)
? nextState
: Object.assign({}, state, nextState);
// 등록된 모든 리스너에게 상태 변경을 알림
listeners.forEach((listener) => listener(state, previousState));
}
};
// 3. getState: 현재 상태를 반환하는 함수
const getState = () => state;
// 4. getInitialState: 처음 설정된 초기 상태를 반환하는 함수
const getInitialState = () => initialState;
// 5. subscribe: 상태가 변경될 때 실행할 함수를 등록하는 기능
const subscribe = (listener) => {
listeners.add(listener);
// 구독 취소 함수도 반환해줘요.
return () => listeners.delete(listener);
};
// 6. api 객체: 외부에서 이 상태 관리 시스템을 다루기 위한 도구 모음
const api = { setState, getState, getInitialState, subscribe };
// 7. createState 호출로 초기 상태를 설정
// createState 함수는 상태의 초기 값을 만들고, 필요한 기능들을 설정할 수 있게 해줘요.
const initialState = (state = createState(setState, getState, api));
// 최종적으로 상태를 다루기 위한 도구(api)를 반환해요.
return api;
import create from 'zustand';
// 스토어 생성
const useStore = create((set) => ({
count: 0,
user: {
name: 'John',
email: 'john@example.com'
},
increment: () => set((state) => ({ count: state.count + 1 })),
updateEmail: (email) => set((state) => ({
user: { ...state.user, email }
})),
}));
// Component: user.email이 변경될 때마다 리렌더링
function Component() {
const user = useStore(state => state.user); // 전체 user 객체 선택
console.log('Component 렌더링!');
return <div>Username: {user.name}</div>;
}
// Component: user.name이 변경될 때만 리렌더링
function Component() {
const name = useStore(state => state.user.name); // 특정 필드 선택
console.log('Component 렌더링!');
return <div>Username: {name}</div>;
}
import shallow from 'zustand/shallow';
// CountAndUser: count나 user.name 변경 시에만 리렌더링
function CountAndUser() {
const [count, name] = useStore(
(state) => [state.count, state.user.name],
shallow // 배열의 요소를 얕은 비교
);
console.log('CountAndUser 렌더링!');
return (
<div>
<div>Count: {count}</div>
<div>User: {name}</div>
</div>
);
}
// 객체 반환 시 참조 변경 방지
function UserProfile() {
const { name, email } = useStore(
(state) => ({
name: state.user.name,
email: state.user.email
}),
(prev, next) => // shallow의 내부 로직과 같은 연산
prev.name === next.name &&
prev.email === next.email
);
console.log('UserProfile 렌더링!');
return <div>{name} - {email}</div>;
}
state.user.name처럼 구체적인 필드 선택 (selector)useMemo 결합// 복잡한 선택자 메모이제이션 예시
import { useMemo } from 'react';
function Component() {
const selectUser = useMemo(() => (state) => ({
name: state.user.name,
initials: state.user.name[0] + state.user.email[0]
}), []);
const userInfo = useStore(selectUser);
return <div>{userInfo.initials}</div>;
}
useCallback 결합 import React from 'react';
import useStore from './store';
// 자식 컴포넌트 (React.memo 사용)
const Children = React.memo(({ onClick }) => {
console.log('Children 리렌더링!');
return <button onClick={onClick}>클릭</button>;
});
// 부모 컴포넌트
function Parent() {
const increment = useStore(state => state.increment);
// 매 렌더링마다 새 함수 생성
const handleClick = () => {
increment();
};
return <Children onClick={handleClick} />;
}
function Parent() {
const increment = useStore(state => state.increment);
// useCallback으로 재생성 방지
const handleClick = React.useCallback(() => {
increment();
}, [increment]); // increment가 변경될 때만 재생성
return <Children onClick={handleClick} />;
}
Atom 기반 전역 상태 관리 , React Context 기반
여러 개의 atom을 독립적으로 관리 → 상태가 변경될 때 해당 컴포넌트(영역)만 리렌더링 - 최적화
useAtom 훅을 사용해 개별 상태(Atom)를 읽고 수정
Derived state 지원 → 하나의 상태를 기반으로 파생된 상태 생성 가능
Recoil과 유사한 방식 (원자적 상태 관리)
불변성(Immutability) 기본 지원 (useState처럼 개별 상태 관리 가능)
컴포넌트가 많아지면 독립적으로 관리해야하는 상태가 많아지는 것이 단점
import { atom, useAtom } from 'jotai';
// 원자(Atom) 상태 정의
const countAtom = atom(0);
function Counter() {
const [count, setCount] = useAtom(countAtom);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
</div>
);
}
Proxy 객체 기반 상태 관리 → 상태를 직접적으로 변경 가능(mutable)
useSnapshot을 사용하여 상태 변경 자동 감지 ⇒ UI 최적화 가능
reactive()를 사용해 객체 상태를 자동으로 추적
React Hook 없이도 상태 변경이 가능
- 그렇기 때문에 상태변경에 대한 추적이 어려움
import { proxy, useSnapshot } from 'valtio';
// Proxy 기반 상태 생성
const state = proxy({ count: 0 });
function Counter() {
const snap = useSnapshot(state);
return (
<div>
<p>Count: {snap.count}</p>
<button onClick={() => state.count++}>+</button>
<button onClick={() => state.count--}>-</button>
</div>
);
}