--- 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; async function fetchUser(id: string): Promise { 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", postId]); qc.setQueryData(["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()( 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 ( <> setQ(e.target.value)} /> ); } ``` ### Pattern 7 — Suspense + Error Boundary (React 19) ```tsx import { ErrorBoundary } from "react-error-boundary"; import { Suspense } from "react";

Error: {error.message}

}> }>
``` ### Pattern 8 — Discriminated union response ```typescript type ApiResult = | { status: "ok"; data: T } | { status: "error"; code: string; message: string } | { status: "loading" }; function render(r: ApiResult) { switch (r.status) { case "loading": return ; case "error": return ; case "ok": return ; } } ``` ## 매 결정 기준 | 상황 | 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 |