From use-react-query
React Query(TanStack Query) hooks를 도메인별로 일관된 폴더/파일 구조로 생성하거나, 기존 API 로직을 React Query hook으로 리팩토링할 때 사용한다. 프로젝트에 새 도메인의 API + React Query hook 체계가 필요할 때(예: 예약, 공지사항, 회원정보 등), 또는 src/hooks/{domain}/ 패턴을 따르는 코드를 만들 때 반드시 이 스킬을 사용한다. triggered when: 사용자가 "api 만들어줘", "react query hooks 생성", "hooks 파일 구조", "useQuery 추가", "도메인별 hooks", ".queries.ts", ".mutations.ts", "query keys" 등을 언급할 때
How this skill is triggered — by the user, by Claude, or both
Slash command
/use-react-query:use-react-queryThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
이 프로젝트는 도메인별로 `src/hooks/{domain}/` 구조를 준수한다.
이 프로젝트는 도메인별로 src/hooks/{domain}/ 구조를 준수한다.
새 API나 React Query hook을 만들 때는 반드시 이 구조를 따른다.
src/hooks/{domain}/
├── apis.ts # Client-side API (브라우저에서 실행, getSupabase)
├── server-apis.ts # Server-side API (SSR/RSC용, createClient) — 필요할 때만
├── keys.ts # Query key 팩토리
├── queries.ts # useQuery hooks
├── mutations.ts # useMutation hooks — 필요할 때만
├── index.ts # 전체 export
└── useXxx.ts # 커스텀 hook (복합 로직, localStorage 등) — 필요할 때만
순서 중요: apis.ts → server-apis.ts → keys.ts → queries.ts → mutations.ts → index.ts
| 파일 | 네이밍 규칙 | 예시 |
|---|---|---|
| 폴더(도메인) | 소문자·카멜x, 복수x | src/hooks/bookmark |
| keys object | {domain}Keys camelCase | bookmarkKeys |
| query hook | use{QueryPurpose} PascalCase | useBookmarkedShopIds |
| mutation hook | use{MutationPurpose} PascalCase | useToggleBookmark |
| query key factory | {domain}Keys.method() | bookmarkKeys.shopIds() |
| API function | fetch{DataPurpose} 동사前缀 | fetchBookmarkedShopIds |
apis.ts — Client-side APIimport { getSupabase } from '@/lib/supabase/client';
export interface SomethingItem { /* ... */ }
export interface CreateSomethingInput { /* ... */ }
export async function fetchSomethingList(): Promise<SomethingItem[]> {
const supabase = getSupabase();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return [];
const { data, error } = await supabase
.from('table_name')
.select('col1, col2')
.eq('user_id', user.id)
.order('created_at', { ascending: false });
if (error) throw error;
return data ?? [];
}
export async function createSomething(input: CreateSomethingInput): Promise<{ id: string }> {
const supabase = getSupabase();
const { data, error } = await supabase
.from('table_name')
.insert(input)
.select('id')
.single();
if (error) throw error;
return data;
}
export async function deleteSomething(id: string): Promise<void> {
const supabase = getSupabase();
const { error } = await supabase.from('table_name').delete().eq('id', id);
if (error) throw error;
}
규칙:
@/lib/supabase/client에서 getSupabase importgetUser()로 인증 체크, 인증 없으면 early returnthrow errornull 대신 [] 또는 null 명시server-apis.ts — Server-side API (optional)import { createClient } from "@/lib/supabase/server";
export async function fetchSomethingDetail(id: string): Promise<SomethingDetail | null> {
const supabase = await createClient();
// ...
}
규칙:
@/lib/supabase/server에서 createClient import (async)keys.ts — Query Key Factoryexport const bookmarkKeys = {
all: ['bookmarks'] as const,
shopIds: () => [...bookmarkKeys.all, 'shopIds'] as const,
// 필요 시 추가:
// detail: (id: string) => [...bookmarkKeys.all, 'detail', id] as const,
// list: (userId: string) => [...bookmarkKeys.all, 'list', userId] as const,
};
규칙:
as const로 readonly tuple 보장all root key로 시작.all → .method() hierarchyqueries.ts — Query Hooksimport { useQuery } from '@tanstack/react-query';
import { fetchSomethingList } from './apis';
import { somethingKeys } from './keys';
export function useSomethingList() {
return useQuery({
queryKey: somethingKeys.list(),
queryFn: fetchSomethingList,
staleTime: 1000 * 60 * 5, // 5분
});
}
export function useSomethingDetail(id: string) {
return useQuery({
queryKey: somethingKeys.detail(id),
queryFn: () => fetchSomethingDetail(id),
enabled: !!id,
});
}
규칙:
staleTime 기본값 5분 (변동성高的 데이터는 30초 등 조정)enabled로 조건부 쿼리 (id 필요 시 enabled: !!id)refetchInterval, refetchOnWindowFocus 등은 useUnreadExists처럼 폴링이 필요한 경우만mutations.ts — Mutation Hooks with Optimistic Updateimport { useMutation, useQueryClient } from '@tanstack/react-query';
import { toggleBookmark } from './apis';
import { bookmarkKeys } from './keys';
export function useToggleBookmark() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: toggleBookmark,
onMutate: async (shopId: string) => {
await queryClient.cancelQueries({ queryKey: bookmarkKeys.shopIds() });
const prev = queryClient.getQueryData<string[]>(bookmarkKeys.shopIds());
queryClient.setQueryData<string[]>(bookmarkKeys.shopIds(), (old = []) =>
old.includes(shopId)
? old.filter((id) => id !== shopId)
: [...old, shopId],
);
return { prev };
},
onError: (_err, _shopId, context) => {
if (context?.prev !== undefined) {
queryClient.setQueryData(bookmarkKeys.shopIds(), context.prev);
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: bookmarkKeys.shopIds() });
},
});
}
규칙:
cancelQueries → setQueryData → return prev → onError rollback → onSettled invalidateonError에서 context?.prev !== undefined 체크onSettled에서 invalidateQueries (항상 최종 동기화)index.ts — Barrel Exportexport * from './queries';
export * from './mutations';
export * from './apis';
export * from './keys';
// 필요 시:
export * from './useUnreadExists';
useXxx.ts)localStorage + query 조합 등 복잡한 로직일 때 분리.
"use client";
import { useQuery } from "@tanstack/react-query";
import { getSupabase } from "@/lib/supabase/client";
import { notificationKeys } from "./keys";
const LAST_KEY = "jadoc:lastViewedAt";
export function markViewed() {
localStorage.setItem(LAST_KEY, new Date().toISOString());
}
async function checkUnread(): Promise<boolean> {
const supabase = getSupabase();
// ...
return false;
}
export function useUnreadExists() {
return useQuery({
queryKey: [...notificationKeys.all, "unread-exists"],
queryFn: checkUnread,
staleTime: 1000 * 30,
refetchInterval: 1000 * 30,
refetchOnWindowFocus: "always",
});
}
규칙:
"use client" 명시| 상황 | 처리 |
|---|---|
| 인증 필요 | if (!user) throw new Error('로그인이 필요합니다') |
| 단일 조회 없음 | .single() 후 if (error) throw error |
| 목록 조회 | .maybeSingle() 후 null 체크 |
| Insert | .insert().select('id').single() |
| Update | .update(patch).eq('id', id) 후 if (error) throw error |
| Delete | .delete().eq('id', id) 후 if (error) throw error |
src/hooks/{domain}/ 생성keys.ts 먼저 작성 (key hierarchy 설계)apis.ts 작성 (Supabase client 사용, 接口 정의 포함)queries.ts 작성 (useQuery hooks)mutations.ts 필요시 작성 (optimistic update 포함)index.ts 작성 (barrel export)useXxx.ts (localStorage 조합 등)Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub auraworks/my-marketplace --plugin use-react-query