[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,422 @@
|
||||
---
|
||||
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<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 줄임)
|
||||
```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<Store>(
|
||||
(set) => ({ count: 0, ... }),
|
||||
{ name: 'counter-store' }
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
### Immer (mutable syntax)
|
||||
```ts
|
||||
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
|
||||
```ts
|
||||
import { devtools } from 'zustand/middleware';
|
||||
|
||||
const useStore = create(devtools<Store>(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 <button onClick={() => setCount(c => c + 1)}>{count}</button>;
|
||||
}
|
||||
```
|
||||
|
||||
→ 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 <Suspense fallback={<Spinner />}>{user.name}</Suspense>;
|
||||
}
|
||||
```
|
||||
|
||||
### 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 <button onClick={() => state.count++}>{snap.count}</button>;
|
||||
}
|
||||
```
|
||||
|
||||
→ 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<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)
|
||||
```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 (
|
||||
<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)
|
||||
```ts
|
||||
// @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
|
||||
```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 <button onClick={() => store.increment()}>{store.count}</button>;
|
||||
});
|
||||
```
|
||||
|
||||
→ 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 <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)
|
||||
```ts
|
||||
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)
|
||||
```tsx
|
||||
// Server Component
|
||||
export default async function Page() {
|
||||
const data = await fetchData();
|
||||
return <Component data={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
→ 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]]
|
||||
Reference in New Issue
Block a user