codingstairs
노트에듀라이프연락
⌕검색⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

연락하기

로그인 없이도 보낼 수 있어요. 답변이 필요하면 이메일을 함께 적어 주세요.

  • 익명 폼으로 의견 남기기 →
  • ✉ warragon112@gmail.com
  • 카카오톡 오픈채팅 ↗

© 2026 codingstairs

  • 노트
  • 에듀
  • 검색
  • 라이프
  • 연락
  • 약관
  • RSS
  • GitHub
노트›frontend

상태 관리의 철학

2026-04-28 게시· 2026-05-18 갱신·0회 조회

상태 관리의 철학 — 클라이언트 상태와 서버 상태

React 앱의 상태 관리는 라이브러리가 많아 혼란을 부릅니다. 그러나 가장 큰 분기점은 라이브러리 선택이 아니라 상태가 클라이언트에 속하는가, 서버에 속하는가 입니다.

1. 두 종류의 상태

TanStack Query 의 문서가 정리한 구분이 자주 인용됩니다.

클라이언트 상태 (client state) 는 클라이언트가 소유합니다. 버튼이 눌렸는가, 사이드바가 열렸는가, 폼에 무엇을 입력했는가.

서버 상태 (server state) 는 서버가 소유하고 클라이언트는 사본을 들고 있을 뿐입니다. 다른 사용자가 바꿀 수 있고, 시간이 지나면 신선하지 않습니다 (stale).

이 둘을 같은 도구로 다루려 하면 어색해집니다.

  • 서버 상태를 Redux store 에 넣으면 캐시 무효화·백그라운드 재요청·stale 처리를 직접 짜야 합니다.
  • 클라이언트 상태를 React Query 에 욱여넣으면 의미 없는 query key 가 생깁니다.

2. 클라이언트 상태 도구

자리 도구 비고
컴포넌트 안 useState · useReducer React 내장.
컴포넌트 트리 안 Context API 값이 자주 바뀌면 리렌더 비용이 큼.
앱 전역 (가벼움) zustand · jotai · valtio 작고 보일러플레이트 적음.
앱 전역 (구조화) Redux Toolkit · MobX DevTools · 큰 팀에 일관된 패턴.

라이브러리 출처 (라이선스는 모두 MIT):

도구 첫 릴리스 만든이
Redux 2015 Dan Abramov · Andrew Clark.
Redux Toolkit (RTK) 2019 Redux 공식. boilerplate 축소.
MobX 2015 Michel Weststrate. observable 기반.
Recoil 2020, Meta atom/selector. 2025 년 1 월 아카이브 공지로 사실상 동결. 후속은 Jotai.
zustand 2019 pmndrs (Paul Henschel). bear store.
jotai 2020 pmndrs. atom 단위, Recoil 스타일.
valtio 2021 pmndrs. proxy 기반.
XState 2017 David Khourshid. 상태 머신.

zustand 와 jotai 의 모델 차이가 자주 비교됩니다.

  • zustand — 한 store 에 여러 값을 두고 selector 로 구독. "store 중심".
  • jotai — 작은 atom 을 조합. "atom 중심", Recoil 의 영향을 명시적으로 인용.

3. 서버 상태 도구

서버 상태에는 캐시·재요청·낙관적 갱신·중복 제거가 필요합니다.

도구 첫 릴리스 만든이
Apollo Client 2016 GraphQL 위주. 정규화 캐시.
SWR 2019, Vercel "stale-while-revalidate" RFC 5861 의 이름.
React Query → TanStack Query 2020 (RQ v1) Tanner Linsley. v4 (2022) 부터 TanStack 으로 개명.
RTK Query 2021 Redux Toolkit 동봉.
Relay 2015 Meta. GraphQL 전용.

대부분 라이브러리가 다음 패턴을 공유합니다.

const { data, isLoading, error } = useQuery({
  queryKey: ["posts", page],
  queryFn: () => fetch(`/api/posts?p=${page}`).then(r => r.json()),
  staleTime: 60_000,
})
  • queryKey — 캐시 식별자. 같은 키는 같은 데이터.
  • staleTime — "이 시간 동안은 신선하다고 간주" (다시 안 부른다).
  • gcTime / cacheTime — 사용되지 않는 캐시를 메모리에서 비우는 시간.
  • invalidate — 변경 후 관련 키를 무효화해 다음 사용 시점에 재요청.

4. Server Component 와의 관계

React Server Components (Next.js App Router 등) 가 등장하면서 서버 상태의 일부는 render 타임에 서버에서 직접 읽습니다. 이 경우 클라이언트 측 캐시 라이브러리가 필요 없을 수 있습니다.

export default async function Page() {
  const posts = await db.post.findMany()
  return <List items={posts} />
}

다만 인터랙션 가능한 상태 (낙관적 갱신 · 폴링 · 무한 스크롤) 는 여전히 클라이언트 캐시가 필요합니다. SWR/TanStack Query 와 RSC 는 경쟁이 아니라 분담에 가깝습니다.

또한 19 의 useOptimistic · useActionState 가 작은 폼 수준의 서버 상태를 다루는 표준 자리를 만들고 있습니다.

5. zustand

import { create } from "zustand"

type Store = {
  count: number
  inc: () => void
}

export const useCounter = create<Store>((set) => ({
  count: 0,
  inc: () => set((s) => ({ count: s.count + 1 })),
}))

// 컴포넌트
const count = useCounter((s) => s.count)
const inc = useCounter((s) => s.inc)

6. TanStack Query

import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query"

const qc = new QueryClient()

export function Root() {
  return (
    <QueryClientProvider client={qc}>
      <App />
    </QueryClientProvider>
  )
}

function Posts() {
  const { data, isPending } = useQuery({
    queryKey: ["posts"],
    queryFn: () => fetch("/api/posts").then(r => r.json()),
    staleTime: 30_000,
  })
  if (isPending) return <p>...</p>
  return <ul>{data.map((p: any) => <li key={p.id}>{p.title}</li>)}</ul>
}

7. SWR

import useSWR from "swr"

const fetcher = (url: string) => fetch(url).then(r => r.json())

function Posts() {
  const { data, error, isLoading } = useSWR("/api/posts", fetcher)
  if (isLoading) return <p>...</p>
  if (error) return <p>error</p>
  return <ul>{data.map((p: any) => <li key={p.id}>{p.title}</li>)}</ul>
}

8. 자주 걸리는 자리

서버 상태를 Redux 에 넣음 — 가능은 하지만 캐시 무효화·재요청을 수동으로 짜야 합니다. 큰 비용입니다.

useEffect 안에서 fetch — 작은 앱은 괜찮지만 동시성·취소·중복 제거를 직접 다뤄야 합니다. 페이지가 늘어날수록 SWR 또는 TanStack Query 같은 도구가 가벼워집니다.

Context 의 과사용 — 한 Context 가 자주 바뀌면 그 트리 전체가 리렌더됩니다. zustand · jotai 의 selector 가 이 자리를 잘 메웁니다.

queryKey 설계 — 키가 너무 거칠면 무효화 범위가 넓어지고, 너무 잘게 나누면 invalidate 조합이 폭발합니다. 자원 + 식별자 + 필터 조합이 권장되는 출발점입니다.

localStorage 동기화의 SSR 문제 — 서버에서는 window 가 없습니다. zustand persist 미들웨어 등도 hydration mismatch 를 일으킬 수 있어 if (typeof window !== "undefined") 가드 또는 useEffect 동기화가 필요합니다.

stale-while-revalidate 의 의미 — "오래된 응답을 잠시 보여주고 백그라운드에서 새로 받는다" 는 RFC 5861 의 캐시 모델. SWR 의 이름이 여기서 왔습니다.

하고픈 말

상태 관리는 도구 선택보다 "이 상태가 어디 속하는가" 의 결정이 더 큰 차이를 만듭니다. 클라이언트 상태와 서버 상태를 처음부터 다른 도구로 다루는 흐름이 운영 부담을 줄입니다. RSC 가 등장하면서 서버 상태의 일부가 더 가까이 왔지만 인터랙션이 있는 자리는 여전히 클라이언트 캐시가 답입니다.

Next

  • styling-tailwind
  • tauri-over-electron

TanStack Query 문서 · SWR 공식 · Redux Toolkit · zustand · Jotai · RFC 5861 · Choosing the State Structure · XState 를 참고합니다.

frontend 카테고리의 다른 글

카테고리 전체 보기 →
  • 도메인 위젯의 통일성 — 4개 도메인에 3개 위젯만 두지 마라
  • 관리자 UI — ResourceTable SSOT 패턴
  • 페이지 로딩 UX
  • 네이티브 통합 — OS 기능들
  • OCR · STT · TTS
  • SQLite — 로컬 앱의 단일 파일 DB