--- id: frontend-state-management-modern title: State Management — Zustand / Jotai / Valtio / Redux Toolkit category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [frontend, state, react, vibe-coding] tech_stack: { language: "TS / React", applicable_to: ["Frontend"] } applied_in: [] aliases: [Zustand, Jotai, Valtio, Redux Toolkit, RTK, Recoil, MobX, signals, state management] --- # State Management Modern > Redux 의 시대 끝. **Zustand (가장 인기), Jotai (atomic), Valtio (proxy), RTK (Redux modern)**. Server state = TanStack Query. Local state = useState. ## 📖 핵심 개념 - Server state ≠ client state. - Server state = TanStack Query / SWR. - Client state = Zustand / Jotai / Context. - Form state = react-hook-form. ## 💻 코드 패턴 ### Zustand (가장 인기) ```ts import { create } from 'zustand'; interface Store { count: number; increment: () => void; reset: () => void; } const useStore = create((set) => ({ count: 0, increment: () => set((s) => ({ count: s.count + 1 })), reset: () => set({ count: 0 }), })); // 사용 function Counter() { const { count, increment } = useStore(); return ; } ``` → Simple, hooks-friendly. ### Selector (re-render 줄임) ```ts const count = useStore((s) => s.count); // count 변경 시 만 re-render // vs const { count } = useStore(); // 매 변경 re-render ``` ### Persist (localStorage) ```ts import { persist } from 'zustand/middleware'; const useStore = create( persist( (set) => ({ count: 0, ... }), { name: 'counter-store' } ) ); ``` ### Immer (mutable syntax) ```ts import { immer } from 'zustand/middleware/immer'; const useStore = create( immer((set) => ({ items: [], addItem: (item) => set((s) => { s.items.push(item); }), })) ); ``` → Mutable syntax, Immer 가 immutable 변경. ### DevTools ```ts import { devtools } from 'zustand/middleware'; const useStore = create(devtools(set => ({ ... }))); ``` → Redux DevTools. ### Subscribe (no re-render) ```ts useStore.subscribe((state) => { console.log('changed:', state); }); ``` ### Jotai (atomic) ```ts import { atom, useAtom } from 'jotai'; const countAtom = atom(0); function Counter() { const [count, setCount] = useAtom(countAtom); return ; } ``` → Atom = unit of state. Granular re-render. ### Derived atom ```ts const doubleAtom = atom((get) => get(countAtom) * 2); function Display() { const [double] = useAtom(doubleAtom); // count 변경 시 만 update } ``` ### Async atom ```ts const userAtom = atom(async (get) => { const id = get(userIdAtom); return fetchUser(id); }); function User() { const [user] = useAtom(userAtom); // Suspense 친화 return }>{user.name}; } ``` ### Atom families ```ts import { atomFamily } from 'jotai/utils'; const userAtomFamily = atomFamily((id: string) => atom(async () => fetchUser(id))); const [user] = useAtom(userAtomFamily('123')); ``` → Per-id atom. ### Valtio (proxy) ```ts import { proxy, useSnapshot } from 'valtio'; const state = proxy({ count: 0 }); function Counter() { const snap = useSnapshot(state); return ; } ``` → Mutable syntax. Proxy 가 변경 감지. ### Redux Toolkit (modern Redux) ```ts import { createSlice, configureStore } from '@reduxjs/toolkit'; const counterSlice = createSlice({ name: 'counter', initialState: { value: 0 }, reducers: { increment: (state) => { state.value++; }, // Immer 자동 incrementBy: (state, action) => { state.value += action.payload; }, }, }); export const { increment, incrementBy } = counterSlice.actions; const store = configureStore({ reducer: { counter: counterSlice.reducer }, }); // 사용 import { useSelector, useDispatch } from 'react-redux'; const value = useSelector((s: RootState) => s.counter.value); const dispatch = useDispatch(); dispatch(increment()); ``` → Boilerplate ↓. 큰 enterprise 친화. ### RTK Query (built-in fetch) ```ts import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; const api = createApi({ reducerPath: 'api', baseQuery: fetchBaseQuery({ baseUrl: '/api' }), endpoints: (builder) => ({ getUser: builder.query({ query: (id) => `users/${id}`, }), }), }); export const { useGetUserQuery } = api; // Component const { data, isLoading } = useGetUserQuery('123'); ``` → Redux 의 built-in TanStack Query 식. ### TanStack Query (server state default) ```ts const { data, isLoading } = useQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id), }); ``` → Cache + retry + refetch + stale 자동. → [[React_TanStack_Query_Advanced]]. ### Context (React built-in) ```tsx const ThemeContext = createContext<'light' | 'dark'>('light'); function App() { const [theme, setTheme] = useState('light'); return ( ); } function Component() { const theme = useContext(ThemeContext); return
...
; } ``` → 작은 / static state 만. 자주 변경 = re-render 폭발. ### Signals (React 18+ proposal) ```ts // @preact/signals-react import { signal, computed } from '@preact/signals-react'; const count = signal(0); const double = computed(() => count.value * 2); function Counter() { return ; } ``` → Signal-based reactivity (Solid / Vue 식). ### MobX ```ts import { observable, action } from 'mobx'; import { observer } from 'mobx-react-lite'; class Store { count = 0; constructor() { makeAutoObservable(this); } increment() { this.count++; } } const store = new Store(); const Counter = observer(() => { return ; }); ``` → Observable + reaction. OOP 친화. ### Recoil (legacy, 거의 dead) ``` Facebook 의 atomic state. 2024 가 Jotai 가 추월. ``` ### Effector (강력) ```ts import { createStore, createEvent } from 'effector'; const increment = createEvent(); const $count = createStore(0).on(increment, (state) => state + 1); function Counter() { const count = useUnit($count); return ; } ``` → Powerful, 작은 ecosystem. ### Library 비교 ``` Zustand: 가장 인기, simple. Jotai: atomic, granular. Valtio: proxy, mutable. RTK: Redux modern. MobX: OOP. Effector: powerful, niche. Signals: reactivity primitive. Context: React built-in (작은). ``` ### 의사결정 ``` Server state: TanStack Query / SWR / RTK Query. Form state: react-hook-form / TanStack Form. URL state: nuqs / TanStack Router search. Theme / auth: Context (작은). 큰 client state: Zustand / Jotai. Enterprise / 큰 팀: RTK. ``` ### URL state (nuqs) ```ts import { useQueryState } from 'nuqs'; function Filter() { const [page, setPage] = useQueryState('page', { defaultValue: '1' }); return setPage(e.target.value)} />; } ``` → URL 가 source of truth. Bookmarkable / sharable. ### Server-side state (Next.js) ```tsx // Server Component export default async function Page() { const data = await fetchData(); return ; } ``` → Server state 가 client X. ### Combine ```ts // Zustand for UI state const useUIStore = create(...); // TanStack Query for server state const { data } = useQuery(...); // Form library const { register } = useForm(); ``` → 매 state 의 적합 도구. ### Performance ``` Re-render 줄이기: - Selector (Zustand / Redux). - Atom split (Jotai). - Memoize component. - React Compiler (auto). ``` ### 함정 ``` - 모든 거 Redux: boilerplate 폭발. - Server state in client store: stale 자주. - Context 큰 store: re-render 폭발. - Zustand 의 entire state subscribe: re-render. - Jotai atom 가 너무 많음: complexity. ``` ### Migration ``` Redux → Zustand: 점진. 1 slice 씩. Recoil → Jotai: API 비슷. MobX → Zustand: rewrite. ``` ## 🤔 의사결정 기준 | 상황 | 추천 | |---|---| | 일반 React | Zustand + TanStack Query | | Atomic / granular | Jotai | | Mutable syntax | Valtio | | 큰 enterprise | RTK | | OOP | MobX | | Server state | TanStack Query / SWR | | URL | nuqs | | Form | RHF / TanStack Form | | Theme / static | Context | ## ❌ 안티패턴 - **모든 거 Redux/Zustand**: server state 가 stale. - **Context 큰 + 자주 변경**: re-render 폭발. - **Selector 없이 useStore()**: 매 변경 re-render. - **Server state in store**: cache 깨짐. - **Form in store**: re-render 폭발. - **Redux + Zustand 둘 다**: 분리 안 됨. ## 🤖 LLM 활용 힌트 - Zustand 가 default modern. - TanStack Query 가 server state. - Jotai 가 granular. - 매 state 의 적합 도구 (4-5 종 mix). ## 🔗 관련 문서 - [[React_State_Library_Comparison]] - [[React_TanStack_Query_Advanced]] - [[Frontend_Form_State_Deep]]