Files
2nd/10_Wiki/Topics/Coding/Frontend_State_Management_Modern.md
T
2026-05-10 22:08:15 +09:00

9.0 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
frontend-state-management-modern State Management — Zustand / Jotai / Valtio / Redux Toolkit Coding draft B conceptual 2026-05-09 2026-05-09
frontend
state
react
vibe-coding
language applicable_to
TS / React
Frontend
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 (가장 인기)

import { create } from 'zustand';

interface Store {
  count: number;
  increment: () => void;
  reset: () => void;
}

const useStore = create<Store>((set) => ({
  count: 0,
  increment: () => set((s) => ({ count: s.count + 1 })),
  reset: () => set({ count: 0 }),
}));

// 사용
function Counter() {
  const { count, increment } = useStore();
  return <button onClick={increment}>{count}</button>;
}

→ Simple, hooks-friendly.

Selector (re-render 줄임)

const count = useStore((s) => s.count);  // count 변경 시 만 re-render

// vs
const { count } = useStore();   // 매 변경 re-render

Persist (localStorage)

import { persist } from 'zustand/middleware';

const useStore = create(
  persist<Store>(
    (set) => ({ count: 0, ... }),
    { name: 'counter-store' }
  )
);

Immer (mutable syntax)

import { immer } from 'zustand/middleware/immer';

const useStore = create(
  immer<Store>((set) => ({
    items: [],
    addItem: (item) => set((s) => { s.items.push(item); }),
  }))
);

→ Mutable syntax, Immer 가 immutable 변경.

DevTools

import { devtools } from 'zustand/middleware';

const useStore = create(devtools<Store>(set => ({ ... })));

→ Redux DevTools.

Subscribe (no re-render)

useStore.subscribe((state) => {
  console.log('changed:', state);
});

Jotai (atomic)

import { atom, useAtom } from 'jotai';

const countAtom = atom(0);

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

→ Atom = unit of state. Granular re-render.

Derived atom

const doubleAtom = atom((get) => get(countAtom) * 2);

function Display() {
  const [double] = useAtom(doubleAtom);
  // count 변경 시 만 update
}

Async atom

const userAtom = atom(async (get) => {
  const id = get(userIdAtom);
  return fetchUser(id);
});

function User() {
  const [user] = useAtom(userAtom);  // Suspense 친화
  return <Suspense fallback={<Spinner />}>{user.name}</Suspense>;
}

Atom families

import { atomFamily } from 'jotai/utils';

const userAtomFamily = atomFamily((id: string) => atom(async () => fetchUser(id)));

const [user] = useAtom(userAtomFamily('123'));

→ Per-id atom.

Valtio (proxy)

import { proxy, useSnapshot } from 'valtio';

const state = proxy({ count: 0 });

function Counter() {
  const snap = useSnapshot(state);
  return <button onClick={() => state.count++}>{snap.count}</button>;
}

→ Mutable syntax. Proxy 가 변경 감지.

Redux Toolkit (modern Redux)

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)

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: (builder) => ({
    getUser: builder.query<User, string>({
      query: (id) => `users/${id}`,
    }),
  }),
});

export const { useGetUserQuery } = api;

// Component
const { data, isLoading } = useGetUserQuery('123');

→ Redux 의 built-in TanStack Query 식.

TanStack Query (server state default)

const { data, isLoading } = useQuery({
  queryKey: ['user', id],
  queryFn: () => fetchUser(id),
});

→ Cache + retry + refetch + stale 자동.

React_TanStack_Query_Advanced.

Context (React built-in)

const ThemeContext = createContext<'light' | 'dark'>('light');

function App() {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={theme}>
      <Component />
    </ThemeContext.Provider>
  );
}

function Component() {
  const theme = useContext(ThemeContext);
  return <div className={theme}>...</div>;
}

→ 작은 / static state 만. 자주 변경 = re-render 폭발.

Signals (React 18+ proposal)

// @preact/signals-react
import { signal, computed } from '@preact/signals-react';

const count = signal(0);
const double = computed(() => count.value * 2);

function Counter() {
  return <button onClick={() => count.value++}>{count}</button>;
}

→ Signal-based reactivity (Solid / Vue 식).

MobX

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 <button onClick={() => store.increment()}>{store.count}</button>;
});

→ Observable + reaction. OOP 친화.

Recoil (legacy, 거의 dead)

Facebook 의 atomic state.
2024 가 Jotai 가 추월.

Effector (강력)

import { createStore, createEvent } from 'effector';

const increment = createEvent();
const $count = createStore(0).on(increment, (state) => state + 1);

function Counter() {
  const count = useUnit($count);
  return <button onClick={() => increment()}>{count}</button>;
}

→ 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)

import { useQueryState } from 'nuqs';

function Filter() {
  const [page, setPage] = useQueryState('page', { defaultValue: '1' });
  return <input value={page} onChange={(e) => setPage(e.target.value)} />;
}

→ URL 가 source of truth. Bookmarkable / sharable.

Server-side state (Next.js)

// Server Component
export default async function Page() {
  const data = await fetchData();
  return <Component data={data} />;
}

→ Server state 가 client X.

Combine

// 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).

🔗 관련 문서