소스 정보
- 저장소
- puk0806/gugbab-claude
- 최근 소스 활동
- 2026년 8월 26일 08:04
- 감지된 SKILL.md 언어
- 한국어
- 스타
- 2
- 포크
- 0
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
메뉴
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/puk0806/gugbab-claude --skill state-management명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
SKILL.md 표시 중
| name | state-management |
| description | Zustand v5 전역 상태관리, TanStack Query v5 서버 상태/캐싱 전략, 상태 레이어 분리 아키텍처 |
소스: https://zustand.docs.pmnd.rs | https://tanstack.com/query/v5/docs 검증일: 2026-08-26 (최초 2026-06-20 · 08-26 freshness 재검증: Zustand 5.0.15·TanStack Query 5.102 최신, API 변경 없음. v4→v5 전환 상세는
frontend/tanstack-query-v4-to-v5-migration이 정본)
상태 종류?
├─ 서버에서 오는 데이터 (API 응답, 캐싱 필요)
│ └─ TanStack Query (useQuery, useMutation)
│
├─ 클라이언트 전역 상태 (여러 컴포넌트 공유, 서버 무관)
│ └─ Zustand
│
└─ 특정 컴포넌트 내 지역 상태
└─ useState / useReducer
| 상태 유형 | 도구 | 예시 |
|---|---|---|
| 서버 데이터 | TanStack Query | 유저 목록, 게시글, 프로필 |
| 전역 UI 상태 | Zustand | 사이드바 열림/닫힘, 선택된 탭, 모달 |
| 인증 상태 | Zustand | 로그인 유저 정보, 토큰 |
| 폼 상태 | React Hook Form | 폼 입력값, 유효성 |
| 지역 상태 | useState | 버튼 hover, 토글 |
❌ 피해야 할 패턴: 서버 데이터를 Zustand에 저장 → TanStack Query가 캐싱/동기화를 더 잘 처리함
// store/ui.ts
import { create } from 'zustand'
interface SidebarStore {
isOpen: boolean
open: () => void
close: () => void
toggle: () => void
}
export const useSidebarStore = create<SidebarStore>((set) => ({
isOpen: false,
open: () => set({ isOpen: true }),
close: () => set({ isOpen: false }),
toggle: () => set((state) => ({ isOpen: !state.isOpen })),
}))
// ❌ v4 방식
import create from 'zustand' // default import
const useStore = create(...)
// ✅ v5 방식
import { create } from 'zustand' // named import
const useStore = create(...)
// v5 추가: useShallow (shallow compare)
import { useShallow } from 'zustand/react/shallow'
const { open, close } = useStore(useShallow((s) => ({ open: s.open, close: s.close })))
// store/slices/auth.ts
import { StateCreator } from 'zustand'
export interface AuthSlice {
user: User | null
setUser: (user: User | null) => void
clearUser: () => void
}
export const createAuthSlice: StateCreator<AuthSlice> = (set) => ({
user: null,
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
})
// store/slices/ui.ts
export interface UISlice {
sidebarOpen: boolean
toggleSidebar: () => void
}
export const createUISlice: StateCreator<UISlice> = (set) => ({
: ,
: ( ({ : !s. })),
})
{ create }
{ createAuthSlice, }
{ createUISlice, }
= &
useStore = create<>( ({
...(...args),
...(...args),
}))
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
interface SettingsStore {
theme: 'light' | 'dark'
language: string
setTheme: (theme: 'light' | 'dark') => void
}
export const useSettingsStore = create<SettingsStore>()(
devtools( // Redux DevTools 연동
persist( // localStorage 영속화
(set) => ({
theme: 'light',
language: 'ko',
setTheme: (theme) => set({ theme }),
}),
{
name: 'settings-storage', // localStorage 키
partialize: (state) => ({ // 저장할 필드만 선택
theme: state.theme,
language: state.language,
}),
}
),
{ name: 'SettingsStore' } // DevTools에 표시될 이름
)
)
// ❌ 스토어 전체를 구독 → 어떤 값이 바뀌어도 리렌더링
const store = useStore()
// ✅ 필요한 값만 구독
const user = useStore((s) => s.user)
const isOpen = useSidebarStore((s) => s.isOpen)
// ✅ 여러 값 구독 시 useShallow로 객체 비교
import { useShallow } from 'zustand/react/shallow'
const { open, close } = useStore(
useShallow((s) => ({ open: s.open, close: s.close }))
)
// providers/query-provider.tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { useState } from 'react'
export function QueryProvider({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1분: 데이터 신선도 유지 시간
gcTime: 5 * 60 * 1000, // 5분: 캐시 보관 시간 (v4의 cacheTime)
retry: 1,
refetchOnWindowFocus: false,
},
},
})
)
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
// ❌ v4
const { data, isLoading } = useQuery(['users'], fetchUsers)
const { data } = useQuery(['user', id], () => fetchUser(id), {
onSuccess: (data) => console.log(data), // v5에서 제거됨
onError: (err) => console.error(err), // v5에서 제거됨
})
// ✅ v5
const { data, isLoading } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
const { data } = useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
// onSuccess/onError 대신 useEffect나 useMutation callbacks 사용
})
// queries/user.keys.ts — 키 팩토리 패턴
export const userKeys = {
all: ['users'] as const,
lists: () => [...userKeys.all, 'list'] as const,
list: (filters: UserFilter) => [...userKeys.lists(), filters] as const,
details: () => [...userKeys.all, 'detail'] as const,
detail: (id: string) => [...userKeys.details(), id] as const,
}
// 사용
useQuery({ queryKey: userKeys.detail(userId), queryFn: () => fetchUser(userId) })
// 관련 캐시 전체 무효화
queryClient.invalidateQueries({ queryKey: userKeys.all })
// 목록만 무효화
queryClient.invalidateQueries({ queryKey: userKeys.lists() })
const { data, isPending, isError, error, isFetching, isStale } = useQuery({
queryKey: ['posts', filters],
queryFn: () => fetchPosts(filters),
staleTime: 5 * 60 * 1000, // 5분간 캐시 신선도 유지 (refetch 안 함)
gcTime: 10 * 60 * 1000, // 10분 후 캐시 가비지 컬렉션
enabled: !!userId, // userId 있을 때만 실행
placeholderData: keepPreviousData, // 페이지 전환 시 이전 데이터 유지
select: (data) => data.items, // 데이터 변환/선택
refetchInterval: 30 * 1000, // 30초마다 폴링
})
// isPending vs isLoading (v5 차이)
// isPending: 캐시 데이터도 없고 fetching 중
// isLoading: isPending && isFetching (= 첫 번째 로딩)
// queries/post.mutations.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { postKeys } from './post.keys'
export function useCreatePost() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (data: CreatePostInput) => createPost(data),
// 성공 후 관련 캐시 무효화
onSuccess: (newPost) => {
queryClient.invalidateQueries({ queryKey: postKeys.lists() })
},
// 낙관적 업데이트
onMutate: async (newData) => {
await queryClient.cancelQueries({ queryKey: postKeys.lists() })
const previous = queryClient.getQueryData(postKeys.lists())
queryClient.setQueryData(postKeys.lists(), (old: Post[]) => [
...old,
{ ...newData, id: 'temp', createdAt: new Date() },
])
{ previous }
},
: {
queryClient.(postKeys.(), context?.)
},
: {
queryClient.({ : postKeys.() })
},
})
}
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts', 'infinite'],
queryFn: ({ pageParam }) => fetchPosts({ cursor: pageParam, limit: 20 }),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor, // undefined면 마지막 페이지
})
// 전체 아이템 flatten
const posts = data?.pages.flatMap((page) => page.items) ?? []
// app/posts/page.tsx (Server Component)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
async function PostsPage() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: postKeys.lists(),
queryFn: fetchPosts,
})
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<PostList /> {/* Client Component에서 useQuery → 캐시 히트 */}
</HydrationBoundary>
)
}
// ✅ 올바른 역할 분리
// Zustand: 클라이언트 전용 UI 상태
const useModalStore = create<ModalStore>(...)
const useAuthStore = create<AuthStore>(...)
// TanStack Query: 서버 데이터
const { data: posts } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts })
// 조합 예시: 선택된 유저 ID는 Zustand, 유저 데이터는 Query
const selectedUserId = useStore((s) => s.selectedUserId)
const { data: user } = useQuery({
queryKey: userKeys.detail(selectedUserId),
queryFn: () => fetchUser(selectedUserId),
enabled: !!selectedUserId, // 선택된 유저 있을 때만 페칭
})
// ❌ 피해야 할 패턴: 서버 데이터를 Zustand에 저장
const usePostStore = create((set) => ({
posts: [], // ❌ API 응답 데이터를 Zustand에 저장
fetchPosts: async () => {
const data = await api.getPosts()
set({ posts: data }) // ❌ 캐싱/동기화 직접 관리 = 복잡도 증가
},
}))
src/
└── store/
├── index.ts # 통합 스토어 export
├── slices/
│ ├── auth.ts # 인증 슬라이스
│ ├── ui.ts # UI 상태 슬라이스
│ └── settings.ts # 설정 슬라이스
└── queries/
├── user.keys.ts # 쿼리 키 팩토리
├── user.queries.ts # useQuery 훅
├── user.mutations.ts # useMutation 훅
└── post.keys.ts
// ❌ 컴포넌트 외부에서 QueryClient 생성 (Next.js SSR에서 공유됨)
const queryClient = new QueryClient() // 모듈 최상위 → 요청 간 상태 공유 위험
// ✅ useState로 인스턴스당 생성
const [queryClient] = useState(() => new QueryClient())
// ❌ v5에서 onSuccess 사용
useQuery({ queryKey: ['x'], queryFn: fn, onSuccess: cb }) // 타입 에러
// ✅ useEffect 또는 mutation 콜백 사용
const { data } = useQuery(...)
useEffect(() => { if (data) doSomething(data) }, [data])
// ❌ enabled 없이 조건부 쿼리
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId), // userId가 undefined일 때 API 호출됨
})
// ✅ enabled 조건 설정
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId!),
enabled: !!userId,
})