用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/puk0806/gugbab-claude --skill state-management命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
DDD(Domain-Driven Design) 아키텍처 핵심 패턴 - 유비쿼터스 언어, 서브도메인, 바운디드 컨텍스트, Aggregate, Entity/VO, 도메인 서비스/이벤트, 레이어드 아키텍처
대규모 React/Next.js 프로젝트를 layer-first(types/·utils/·hooks/·api/·components/ 밑에 도메인이 반복되는 구조)에서 domain-first(feature/도메인 우선) 구조로 전환하는 설계 기준과 절차. Feature-Sliced Design 2.1 정본(layers 6종·slices·segments·import 규칙·@x 크로스임포트·public API), FSD를 쓰지 않는 경량 대안(features + shared 2~3계층 + ESLint import/no-restricted-paths), Next.js App Router 공존 전략(route group `()`·private folder `_`·colocation), Turborepo/Nx 모노레포에서 폴더↔패키지 승격 기준, colocation과 배럴 파일 성능 트레이드오프, 도메인 경계 역추출(import 그래프·change coupling·용어 클러스터), 전환 실패 패턴(shared 비대화·entities 남용·순환 의존·도메인=라우트 착각·조기 추상화). 도메인 개념 자체(바운디드 컨텍스트·유비쿼터스 언어)는 `architecture/ddd` 스킬을 참조한다.
소스 파일 수천 개 규모 프론트엔드 코드베이스를 멈추지 않고 점진 재구조화하는 실행 전략 - Strangler Fig / Branch by Abstraction / Parallel Change, ts-morph·jscodeshift codemod, PR 분할·검증 게이트·되돌리기, 테스트 없는 코드의 안전망, 작업 순서 설계와 위반 수 기반 진행 추적
正在显示 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,
})