---
id: wiki-2026-0508-상태-관리-및-api-응답-모델링-state-managem
title: 상태 관리 및 API 응답 모델링 (State Management and API Response Modeling)
category: 10_Wiki/Topics
status: verified
canonical_id: self
aliases: [State Management, API Response Modeling, Server State, Client State]
duplicate_of: none
source_trust_level: A
confidence_score: 0.9
verification_status: applied
tags: [state-management, api, react, tanstack-query, zustand, frontend]
raw_sources: []
last_reinforced: 2026-05-10
github_commit: pending
tech_stack:
  language: typescript
  framework: React 19/TanStack Query v5/Zustand
---

# 상태 관리 및 API 응답 모델링

## 매 한 줄
> **"매 server state 와 client state 의 분리 가 modern 의 시작"**. 매 2026 standard React app 에서 server cache (TanStack Query / SWR / RTK Query) + client UI state (Zustand / Jotai / useState) 의 명확한 boundary 가 default — 매 redux-thunk 의 monolithic global store 시대 는 끝났고, 매 API response 는 typed schema (Zod/Valibot) 로 runtime validation 의 거침.

## 매 핵심

### 매 두 종류 의 state
- **Server state**: server 가 source of truth. 매 cache, refetch, invalidate, optimistic update. → TanStack Query / SWR / RTK Query / Apollo.
- **Client state**: UI 만 의 관심. modal open/close, theme, form draft. → useState / Zustand / Jotai / Valtio.
- **URL state**: 매 search/filter 의 URL 동기화 (nuqs, TanStack Router).
- **Form state**: React Hook Form / TanStack Form.

### 매 API response 의 modeling
1. **OpenAPI spec → codegen** (orval, openapi-typescript): 매 타입 + client 의 자동 생성.
2. **tRPC**: 매 server-client end-to-end type. 매 BFF pattern 에 적합.
3. **GraphQL + codegen**: 매 graphql-codegen + Apollo / Relay.
4. **Manual + Zod**: 매 small project 에 OK. 매 runtime validation 가능.

### 매 normalization 의 trade-off
- 매 normalized (entities by id) — 매 RTK Query / Apollo. 매 mutation 시 efficient cache 갱신.
- 매 denormalized (response shape 그대로) — 매 TanStack Query default. 매 simpler.
- 매 2026 trend: 매 simpler default + 매 specific case 만 normalize.

### 매 응용
1. Dashboard with multi-resource (TanStack Query + Zustand).
2. Real-time chat (Query + WebSocket subscription).
3. Optimistic UI (Query mutate with rollback).
4. Offline-first (Query persistor + IndexedDB).

## 💻 패턴

### Pattern 1 — Zod schema + TanStack Query
```typescript
import { z } from "zod";
import { useQuery } from "@tanstack/react-query";

const User = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  name: z.string(),
  createdAt: z.coerce.date(),
});
type User = z.infer<typeof User>;

async function fetchUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error("HTTP " + res.status);
  return User.parse(await res.json());   // runtime check
}

export function useUser(id: string) {
  return useQuery({ queryKey: ["user", id], queryFn: () => fetchUser(id),
                    staleTime: 60_000 });
}
```

### Pattern 2 — Optimistic mutation
```typescript
import { useMutation, useQueryClient } from "@tanstack/react-query";

export function useToggleLike(postId: string) {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: () => fetch(`/api/posts/${postId}/like`, { method: "POST" }),
    onMutate: async () => {
      await qc.cancelQueries({ queryKey: ["post", postId] });
      const prev = qc.getQueryData<Post>(["post", postId]);
      qc.setQueryData<Post>(["post", postId], old =>
        old ? { ...old, liked: !old.liked, likes: old.likes + (old.liked ? -1 : 1) } : old);
      return { prev };
    },
    onError: (_e, _v, ctx) => ctx?.prev && qc.setQueryData(["post", postId], ctx.prev),
    onSettled: () => qc.invalidateQueries({ queryKey: ["post", postId] }),
  });
}
```

### Pattern 3 — Zustand client store
```typescript
import { create } from "zustand";
import { persist } from "zustand/middleware";

type UI = {
  theme: "light" | "dark";
  sidebar: boolean;
  setTheme: (t: UI["theme"]) => void;
  toggleSidebar: () => void;
};

export const useUI = create<UI>()(
  persist(
    set => ({
      theme: "light", sidebar: true,
      setTheme: t => set({ theme: t }),
      toggleSidebar: () => set(s => ({ sidebar: !s.sidebar })),
    }),
    { name: "ui-store" }
  )
);
```

### Pattern 4 — Server / client boundary
```typescript
// hooks/useDashboard.ts
export function useDashboard() {
  const userQ  = useUser("me");                    // server
  const ordersQ = useOrders({ status: "open" });   // server
  const sidebar = useUI(s => s.sidebar);           // client
  return {
    user: userQ.data, orders: ordersQ.data,
    isLoading: userQ.isLoading || ordersQ.isLoading,
    sidebar,
  };
}
```

### Pattern 5 — tRPC end-to-end types
```typescript
// server: routers/post.ts
import { z } from "zod"; import { publicProcedure, router } from "../trpc";
export const postRouter = router({
  list: publicProcedure.input(z.object({ limit: z.number().max(100) }))
    .query(({ input }) => db.post.findMany({ take: input.limit })),
  create: publicProcedure.input(z.object({ title: z.string().min(1) }))
    .mutation(({ input }) => db.post.create({ data: input })),
});

// client
const { data } = trpc.post.list.useQuery({ limit: 20 });   // fully typed
```

### Pattern 6 — URL state (nuqs)
```typescript
import { useQueryState, parseAsString, parseAsInteger } from "nuqs";

function ProductFilters() {
  const [q, setQ] = useQueryState("q", parseAsString.withDefault(""));
  const [page, setPage] = useQueryState("page", parseAsInteger.withDefault(1));
  return (
    <>
      <input value={q} onChange={e => setQ(e.target.value)} />
      <button onClick={() => setPage(page + 1)}>Next</button>
    </>
  );
}
```

### Pattern 7 — Suspense + Error Boundary (React 19)
```tsx
import { ErrorBoundary } from "react-error-boundary";
import { Suspense } from "react";

<ErrorBoundary fallbackRender={({ error }) => <p>Error: {error.message}</p>}>
  <Suspense fallback={<Skeleton />}>
    <Dashboard />
  </Suspense>
</ErrorBoundary>
```

### Pattern 8 — Discriminated union response
```typescript
type ApiResult<T> =
  | { status: "ok";    data: T }
  | { status: "error"; code: string; message: string }
  | { status: "loading" };

function render(r: ApiResult<User>) {
  switch (r.status) {
    case "loading": return <Skeleton />;
    case "error":   return <Err msg={r.message} />;
    case "ok":      return <UserCard u={r.data} />;
  }
}
```

## 매 결정 기준
| 상황 | Tool |
|---|---|
| Server data fetch + cache | TanStack Query (default) |
| Full-stack TS, BFF | tRPC |
| Heavy entity normalization | RTK Query / Apollo |
| Small client UI flag | useState / Zustand |
| Atomic, fine-grained reactivity | Jotai / Valtio |
| Form | React Hook Form |
| URL filter state | nuqs / TanStack Router |
| Runtime validation | Zod / Valibot |

**기본값**: 매 TanStack Query (server) + Zustand (client) + Zod (validation).

## 🔗 Graph
- 부모: [[Frontend Architecture]] · [[React]]
- 변형: [[상태 관리 최적화 (Zustand Jotai Valtio)]] · [[TanStack Query]]
- 응용: [[Dashboard]] · [[Optimistic UI]]
- Adjacent: [[tRPC]] · [[OpenAPI Codegen]] · [[Zod]]

## 🤖 LLM 활용
**언제**: 매 OpenAPI spec → typed client codegen, 매 Zod schema 의 generation, 매 mutation 의 optimistic update boilerplate.
**언제 X**: 매 cache invalidation 의 business decision — 매 domain knowledge 가 필요.

## ❌ 안티패턴
- **useEffect + fetch**: 매 race condition, no cache, no dedup. 매 TanStack Query 가 default.
- **Global redux for everything**: 매 server state 의 cache 의 reinvent.
- **No runtime validation**: 매 backend 변경 시 silent type-lie.
- **Mutation 후 manual refetch all**: 매 invalidation pattern 의 무시.

## 🧪 검증 / 중복
- Verified (TanStack Query v5 docs, tRPC v11, React 19 RFC).
- 신뢰도 A.

## 🕓 Changelog
| 날짜 | 변경 |
|---|---|
| 2026-05-08 | Phase 1 |
| 2026-05-10 | Manual cleanup — server/client state 분리 + 8 patterns |
