Files
2nd/10_Wiki/Topics/AI_and_ML/상태 관리 및 API 응답 모델링(State Management and API Response Modeling).md
T
Antigravity Agent f8b21af4be Wiki cleanup: error-doc removal, dedup merge, link normalization
10_Wiki/Topics 대규모 정리:
- 오류 캡처/미완성 stub 문서 227개 제거
- 교차폴더 중복 43클러스터 병합 (63파일 → redirect)
- 링크명 정규화: 깨진 링크 수정·redirect 직결·개념 매핑 ~2,400건
- 카테고리 MOC 6개 신규 생성
- Graph 섹션 미해결 related-keyword 링크 10,058건 제거

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 23:52:15 +09:00

7.9 KiB

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

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

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

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

// 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

// 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)

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)

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

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

🤖 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