zustand-tanstack
Zustand + TanStack Query 상태 관리 패턴. store 생성, devtools, 서버 상태 연동, 디렉토리 구조.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Zustand + TanStack Query 상태 관리 패턴. store 생성, devtools, 서버 상태 연동, 디렉토리 구조.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
세션 핸드오프. 컨텍스트가 차기 전에 중요 맥락을 progress.md에 문서화하고 새 세션 킥오프 프롬프트를 클립보드에 복사한다. "핸드오프", "세션 정리하고 넘기자", "컨텍스트 정리", "이어갈 준비" 등의 요청에 사용.
MD 파일 또는 텍스트로 작업을 정의하면, 분석 → 디자인 확인 → 구현 → 검증 → 커밋 → PR까지 전체 플로우를 수행한다. "이거 만들어줘", "기능 구현해줘", "작업 시작하자", "이 티켓 진행해줘" 등 코드 작업 착수 발화에 사용.
Claude + OpenAI Codex 협업 스킬. MCP 서버 설정 시에만 사용 가능.
code-forge 상태 대시보드. REFLECT flag, quality 이벤트, notepad/decisions, usage 집계를 한 번에. forge-glow 같은 외부 도구는 --json으로 파싱.
교차 모델 토론. Agent Teams / Codex CLI / self-debate 모드 선택. 설계 결정, 아키텍처 선택 시 활용. "어느 쪽이 나을까", "설계 비교해줘", "토론시켜줘", "교차 검증해줘" 등의 요청에 사용.
화면 단위 E2E 테스트 자동화. Figma/코드 기반 테스트 케이스 도출 → Playwright 코드 생성 → Forge Loop(에스컬레이션 기반 자율 실행). "E2E 돌려줘", "화면 테스트 만들어줘", "이 페이지 자동 테스트" 등의 요청에 사용.
| name | zustand-tanstack |
| description | Zustand + TanStack Query 상태 관리 패턴. store 생성, devtools, 서버 상태 연동, 디렉토리 구조. |
| 상태 유형 | 도구 | 예시 |
|---|---|---|
| 서버 상태 | TanStack Query | 주문 목록, 사용자 정보 |
| 전역 클라이언트 상태 | Zustand | 선택된 탭, 사이드바 열림 여부 |
| 폼 상태 | React Hook Form | 입력값, 유효성 검사 |
| 로컬 UI 상태 | useState | 모달 열림/닫힘 |
store/
├── useAuthStore.ts # 인증 상태
├── useUIStore.ts # UI 전역 상태
└── useFilterStore.ts # 필터/검색 상태
queries/
├── order/
│ ├── index.ts # useOrderQuery, useOrderMutation
│ └── queryKeys.ts # 쿼리 키 팩토리
└── user/
├── index.ts
└── queryKeys.ts
// store/useAuthStore.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
interface User {
id: string;
name: string;
email: string;
}
interface AuthState {
user: User | null;
isAuthenticated: boolean;
// Actions
login: (user: User) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>()(
devtools(
persist(
(set) => ({
user: null,
isAuthenticated: false,
login: (user) => set({ user, isAuthenticated: true }, false, 'login'),
logout: () => set({ user: null, isAuthenticated: false }, false, 'logout'),
}),
{
name: 'auth-storage', // localStorage key
partialize: (state) => ({ user: state.user }), // 저장할 필드만 선택
},
),
{ name: 'AuthStore' }, // DevTools 이름
),
);
// store/useUIStore.ts
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
interface UIState {
isSidebarOpen: boolean;
activeTab: string;
// Actions
toggleSidebar: () => void;
setActiveTab: (tab: string) => void;
}
export const useUIStore = create<UIState>()(
devtools(
(set) => ({
isSidebarOpen: true,
activeTab: 'overview',
toggleSidebar: () =>
set((state) => ({ isSidebarOpen: !state.isSidebarOpen }), false, 'toggleSidebar'),
setActiveTab: (tab) =>
set({ activeTab: tab }, false, 'setActiveTab'),
}),
{ name: 'UIStore' },
),
);
// ✅ 좋은 예: selector로 필요한 상태만 구독
function Header() {
const user = useAuthStore((state) => state.user);
const logout = useAuthStore((state) => state.logout);
return <div>{user?.name} <button onClick={logout}>로그아웃</button></div>;
}
// ❌ 나쁜 예: 전체 state 구독 (불필요한 리렌더링 발생)
function Header() {
const { user, logout } = useAuthStore();
}
// ✅ 여러 값을 한번에 구독할 때 (shallow 비교)
import { useShallow } from 'zustand/react/shallow';
function FilterPanel() {
const { status, dateRange, setStatus } = useFilterStore(
useShallow((state) => ({
status: state.status,
dateRange: state.dateRange,
setStatus: state.setStatus,
})),
);
}
// queries/order/queryKeys.ts
export const orderKeys = {
all: ['orders'] as const,
lists: () => [...orderKeys.all, 'list'] as const,
list: (filters: OrderFilters) => [...orderKeys.lists(), filters] as const,
details: () => [...orderKeys.all, 'detail'] as const,
detail: (id: string) => [...orderKeys.details(), id] as const,
};
// queries/order/index.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { orderKeys } from './queryKeys';
import { fetchOrders, fetchOrder, createOrder, updateOrder } from '@/services/order';
export function useOrderListQuery(filters: OrderFilters) {
return useQuery({
queryKey: orderKeys.list(filters),
queryFn: () => fetchOrders(filters),
staleTime: 5 * 60 * 1000, // 5분
});
}
export function useOrderDetailQuery(id: string) {
return useQuery({
queryKey: orderKeys.detail(id),
queryFn: () => fetchOrder(id),
enabled: !!id, // id가 있을 때만 실행
});
}
export function useCreateOrderMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createOrder,
onSuccess: () => {
// 목록 캐시 무효화
queryClient.invalidateQueries({ queryKey: orderKeys.lists() });
},
});
}
// Zustand 필터 상태 + TanStack Query 서버 상태 조합
function OrderListPage() {
// 클라이언트 상태 (Zustand)
const filters = useFilterStore(useShallow((s) => ({
status: s.status,
dateRange: s.dateRange,
})));
// 서버 상태 (TanStack Query)
const { data, isLoading } = useOrderListQuery(filters);
return <OrderList orders={data?.orders} loading={isLoading} />;
}
// src/main.tsx 또는 providers/QueryProvider.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1분
retry: 1,
refetchOnWindowFocus: false,
},
},
});
function Providers({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
devtools 미들웨어 적용?useShallow 사용?queryKeys 팩토리로 관리?invalidateQueries 처리?