[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
---
|
||||
id: react-tanstack-query-advanced
|
||||
title: TanStack Query 심화 — invalidate / optimistic / prefetch
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [react, tanstack-query, react-query, vibe-coding]
|
||||
tech_stack: { language: "TypeScript / React", applicable_to: ["Frontend"] }
|
||||
applied_in: []
|
||||
aliases: [react-query, useMutation, optimistic update, query invalidation, queryKey]
|
||||
---
|
||||
|
||||
# TanStack Query — Advanced
|
||||
|
||||
> Server state 의 표준. **queryKey 는 array + 안정**, mutation 의 `onMutate`/`onError`/`onSettled` 가 optimistic, `setQueryData` + `invalidateQueries` 가 cache 동기화.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- queryKey: 캐시 식별 — 변경 시 새 쿼리.
|
||||
- Stale: 데이터가 outdated 상태. Refetch trigger.
|
||||
- Mutation: write — `onMutate` 로 cache 미리 갱신 (optimistic).
|
||||
- Hydration: SSR / RSC 의 dehydrated state 클라에 주입.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### queryKey 표준화 (factories)
|
||||
```ts
|
||||
// queries/users.ts
|
||||
export const userKeys = {
|
||||
all: ['users'] as const,
|
||||
lists: () => [...userKeys.all, 'list'] as const,
|
||||
list: (filter: UserFilter) => [...userKeys.lists(), filter] as const,
|
||||
details: () => [...userKeys.all, 'detail'] as const,
|
||||
detail: (id: string) => [...userKeys.details(), id] as const,
|
||||
};
|
||||
|
||||
useQuery({ queryKey: userKeys.detail(id), queryFn: () => api.user.get(id) });
|
||||
queryClient.invalidateQueries({ queryKey: userKeys.lists() }); // 모든 list invalidate
|
||||
```
|
||||
|
||||
### Optimistic update
|
||||
```tsx
|
||||
const qc = useQueryClient();
|
||||
|
||||
const mut = useMutation({
|
||||
mutationFn: (patch: PostPatch) => api.post.update(patch.id, patch),
|
||||
onMutate: async (patch) => {
|
||||
// 1. 진행중 query cancel
|
||||
await qc.cancelQueries({ queryKey: postKeys.detail(patch.id) });
|
||||
|
||||
// 2. 이전 값 백업
|
||||
const prev = qc.getQueryData<Post>(postKeys.detail(patch.id));
|
||||
|
||||
// 3. 새 값으로 미리 set
|
||||
qc.setQueryData<Post>(postKeys.detail(patch.id), old => ({ ...old!, ...patch }));
|
||||
|
||||
return { prev }; // context
|
||||
},
|
||||
onError: (err, patch, ctx) => {
|
||||
if (ctx?.prev) qc.setQueryData(postKeys.detail(patch.id), ctx.prev);
|
||||
},
|
||||
onSettled: (_, __, patch) => {
|
||||
qc.invalidateQueries({ queryKey: postKeys.detail(patch.id) });
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Prefetch (Router/hover)
|
||||
```ts
|
||||
// route loader
|
||||
await qc.prefetchQuery({ queryKey: userKeys.detail(id), queryFn: ... });
|
||||
|
||||
// hover
|
||||
<Link to={`/u/${id}`} onMouseEnter={() => qc.prefetchQuery(...)}>...</Link>
|
||||
```
|
||||
|
||||
### Infinite query
|
||||
```tsx
|
||||
const q = useInfiniteQuery({
|
||||
queryKey: postKeys.lists(),
|
||||
queryFn: ({ pageParam }) => api.post.list({ cursor: pageParam }),
|
||||
initialPageParam: undefined,
|
||||
getNextPageParam: (last) => last.nextCursor,
|
||||
});
|
||||
|
||||
const items = q.data?.pages.flatMap(p => p.items) ?? [];
|
||||
|
||||
<button onClick={() => q.fetchNextPage()} disabled={!q.hasNextPage}>load more</button>
|
||||
```
|
||||
|
||||
### Suspense + ErrorBoundary
|
||||
```tsx
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<ErrorBoundary fallbackRender={({ error, resetErrorBoundary }) => ...}>
|
||||
<UserPanel id={id} />
|
||||
</ErrorBoundary>
|
||||
</Suspense>
|
||||
|
||||
function UserPanel({ id }) {
|
||||
const { data } = useSuspenseQuery({ queryKey: userKeys.detail(id), queryFn: ... });
|
||||
return <div>{data.name}</div>; // data 는 항상 정의됨
|
||||
}
|
||||
```
|
||||
|
||||
### 글로벌 default
|
||||
```ts
|
||||
const qc = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 30_000,
|
||||
gcTime: 5 * 60_000,
|
||||
retry: (count, err) => count < 2 && !isClientError(err),
|
||||
refetchOnWindowFocus: 'always',
|
||||
},
|
||||
mutations: {
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### SSR (Next.js)
|
||||
```tsx
|
||||
// server component / loader
|
||||
export default async function Page() {
|
||||
const qc = new QueryClient();
|
||||
await qc.prefetchQuery({ queryKey: userKeys.detail('1'), queryFn: ... });
|
||||
|
||||
return (
|
||||
<HydrationBoundary state={dehydrate(qc)}>
|
||||
<ClientPage id="1" />
|
||||
</HydrationBoundary>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Dependent queries
|
||||
```tsx
|
||||
const { data: user } = useQuery({ queryKey: userKeys.detail(id), queryFn: ... });
|
||||
const { data: posts } = useQuery({
|
||||
queryKey: postKeys.list({ userId: user?.id }),
|
||||
queryFn: () => api.post.list({ userId: user!.id }),
|
||||
enabled: !!user,
|
||||
});
|
||||
```
|
||||
|
||||
### Selective updates (setQueriesData)
|
||||
```ts
|
||||
qc.setQueriesData<User[]>({ queryKey: userKeys.lists() }, (old) =>
|
||||
old?.map(u => u.id === updated.id ? updated : u)
|
||||
);
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 상황 | 권장 |
|
||||
|---|---|
|
||||
| Server state | TanStack Query |
|
||||
| Client-only state | Zustand / useState |
|
||||
| Form state | RHF |
|
||||
| Realtime | useQuery + WebSocket invalidate |
|
||||
| Pagination | useInfiniteQuery |
|
||||
| 즉시 반응 UI | Optimistic update |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **queryKey unstable (객체 inline)**: 매 렌더 다른 key — 무한 fetch.
|
||||
- **Mutation 후 setQueryData 없이 invalidate 만**: refetch 도착까지 빈 화면.
|
||||
- **Optimistic onError 복원 X**: 실패 시 잘못된 cache.
|
||||
- **staleTime 0**: 매 마운트 refetch — 과도.
|
||||
- **Global state 에 server data 복사**: 두 source of truth.
|
||||
- **enabled false 인데 queryFn 무거움**: ... 그래도 기본 안전.
|
||||
- **gcTime 너무 길음 (24h)**: 메모리 누적.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- queryKey factory + invalidate hierarchy.
|
||||
- Mutation 4단계 (cancel/backup/set/onSettled invalidate).
|
||||
- SSR = HydrationBoundary.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[React_Suspense_for_Data]]
|
||||
- [[React_Form_State_Patterns]]
|
||||
- [[React_Server_Components]]
|
||||
Reference in New Issue
Block a user