본문으로 건너뛰기
yutils

state management 는 어떻게 동작할까?

local vs global state, Context API 가 Redux 대체 아닌 이유 (re-render 성능), Zustand vs Jotai vs Redux 차이, server state vs client state (TanStack Query / SWR), '라이브러리 없음' 이 정답인 경우.

약 9분 읽기

모든 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) 만.

가이드 목록으로