모든 React 프로젝트의 첫 50 KB 의 선택 — Redux? Zustand? Jotai? Context? "그냥 useState 쓰지" — 그러다 props drilling. 그러면 Context? — 그러다 re-render 폭증. 이 가이드는 각 도구의 trade-off, server state vs client state 의 구분, "라이브러리 없음" 이 정답인 경우를 정리한다.
Local vs Global — 첫 질문
local state (useState):
- 한 component + 그 자식 일부에서만 사용
- 단순 — useState 만으로 충분
- 예: form input, modal open/close, hover state
global state:
- 여러 page / 멀리 떨어진 component 가 공유
- 예: 로그인 사용자, theme, cart, notification
→ 95% 는 local 로 시작. global 필요한 게 명확해질 때만 lift up 또는 store.Context API — Redux 대체 아님
흔한 사용:
const UserContext = createContext(null);
function App() {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{user, setUser}}>
<Header /> ← 모든 자식이 user 접근 가능
<Main />
</UserContext.Provider>
);
}
문제 — Context value 가 변경될 때:
- useContext 사용하는 모든 자손 component re-render
- object identity 가 매 render 마다 새로워 (= new reference) → 무조건 re-render
// ❌ value 가 매 render 마다 새 객체
<UserContext.Provider value={{user, setUser}}>
// ✓ useMemo 로 안정화
const value = useMemo(() => ({user, setUser}), [user]);
<UserContext.Provider value={value}>
// 그래도 user 변경 시 모든 사용처 re-render
→ 한 component 만 user.name 사용해도 다른 component 다 re-render
→ Context 는 "props drilling 회피" 용. 자주 변경되는 state 에는 부적합.
Redux / Zustand / Jotai 같은 dedicated store 는 fine-grained subscription.Redux — 전통 강자
// 단일 store + action + reducer
const store = createStore(rootReducer);
store.dispatch({type: "SET_USER", payload: user});
// React 통합 (react-redux)
const user = useSelector(state => state.user);
장점:
- 강한 패턴 (action / reducer / selector)
- DevTools (time travel debugging)
- middleware (saga, thunk)
- selector 가 변경된 일부만 re-render 보장
단점:
- boilerplate 큼 (action type 정의, reducer 작성, selector)
→ Redux Toolkit (RTK) 으로 줄어듦
- 학습 곡선
- 모든 state 가 store 에 → 작은 local 도 다 박는 anti-pattern 우려
modern 권장: Redux Toolkit (RTK) + RTK Query — Redux 의 강점 유지 + 단순화Zustand — minimal 진영
import {create} from "zustand";
const useUserStore = create((set) => ({
user: null,
setUser: (user) => set({user}),
}));
// component 에서:
const user = useUserStore(state => state.user);
const setUser = useUserStore(state => state.setUser);
장점:
- 매우 단순 (action 정의 X, 직접 set 호출)
- bundle 작음 (~1 KB)
- TypeScript 친화
- selector 기반 fine-grained subscription
단점:
- Redux DevTools 통합 가능하지만 default X
- middleware ecosystem Redux 보다 작음
→ 새 프로젝트 시작 시 first choice 권장 (2024+ 추세).Jotai — atomic state
import {atom, useAtom} from "jotai";
const countAtom = atom(0);
const doubledAtom = atom(get => get(countAtom) * 2); // derived
function Counter() {
const [count, setCount] = useAtom(countAtom);
const [doubled] = useAtom(doubledAtom); // auto-recompute
return <button onClick={() => setCount(count+1)}>{count} → {doubled}</button>;
}
장점:
- "atomic" 모델 — 작은 atom 들의 graph (Recoil 의 정신적 후계)
- 자동 dependency tracking (derived atom)
- 매우 작음 (~1 KB)
단점:
- mental model 적응 시간
- 큰 application 의 atom 관리 복잡할 수 있음
용도: form 같이 작은 단위의 reactive state 가 많은 경우.Server State vs Client State — 가장 큰 통찰
client state: UI 의 일시 상태
- modal open, current tab, selected filter
- 새로고침 시 초기화 OK
- useState / useReducer / Zustand
server state: server 의 데이터의 client 캐시
- user 정보, post list, search result
- server 가 source of truth
- cache / refetch / mutation invalidation 필요
→ 두 가지를 같은 도구 (Redux 등) 로 다루면 — server state 의 cache /
loading / error state 를 직접 관리해야 (boilerplate 폭증).
TanStack Query (구 React Query) — server state 전용:
const {data, isLoading, error} = useQuery({
queryKey: ["users"],
queryFn: () => fetch("/api/users").then(r => r.json()),
});
// cache + loading + error + refetch + stale-while-revalidate 자동
SWR (Vercel) — 비슷한 도구.
modern 추세:
- server state: TanStack Query 또는 SWR
- client state: useState (local) + Zustand (global)
- 두 영역 명확히 분리 → boilerplate ↓ + UX ↑"라이브러리 없음" 이 정답인 경우
- 대부분의 작은 app — useState + props 만으로 충분. props drilling 2-3 단계는 견딜 만함.
- 고립된 widget — calculator, color picker 같은 self-contained. local state 만.
- simple form — react-hook-form 또는 useState (form 라이브러리 추가 신중).
- RSC 활용 시 — server state 의 일부가 자연스럽게 server component 의 props 로 (TanStack Query 도 필요 X 인 경우).
흔한 함정
- Context value 가 매 render 마다 새 객체 — performance 폭망. useMemo 안정화 필수.
- 모든 state 를 Redux 에 — modal open 같은 local 도 박으면 store 비대 + 부하. local 은 useState.
- server state 를 직접 store 관리 — cache / refetch 보일러 plate 폭발. TanStack Query 의지.
- premature optimization — 작은 app 에 Redux 전체 setup 은 over-engineering.
- 여러 store 라이브러리 혼용 — Zustand + Redux + Jotai 동시 사용은 인지 부하. 한 두 가지 선택.
마무리
State management 의 큰 통찰 = server state ≠ client state. server state 는 TanStack Query / SWR 의 domain. client state 는 useState (local) + Zustand (global) 의 modern 조합.
실용 — 새 프로젝트 default: useState + TanStack Query. global client state 필요 시 Zustand. legacy / 큰 enterprise = Redux Toolkit. Context 는 정적인 값 (theme, locale) 만.