--- name: nuxt-composable description: Nuxt 3 composable(useXxx)과 데이터 페칭(useFetch/useAsyncData) 패턴을 작성할 때 사용합니다. "composable 만들어줘", "useFetch 패턴", "데이터 페칭", "useAsyncData", "커스텀 훅", "로직 추출" 등을 요청하면 트리거됩니다. --- # Nuxt Composable · 데이터 페칭 이 skill은 Nuxt 3의 composable 함수와 데이터 페칭 패턴을 팀 컨벤션에 맞게 생성합니다. 데이터 페칭 composable(서버 상태)과 로직 composable(UI 상태/행동)을 구분하여 작성합니다. ## 작업 순서 1. **관심사 식별** - 데이터 페칭 composable인가? (API 호출, 서버 상태) - 로직 composable인가? (UI 상태, 재사용 행동) 2. **기존 composable 탐색** - `composables/` 디렉토리에서 기존 패턴 확인 - 중복 생성 방지 3. **네이밍 결정** - `use` 접두사 + PascalCase 도메인명 - 파일: `composables/useXxx.ts` (Nuxt auto-import 대상) - 예: `useAuth`, `useUserProfile`, `useProductList`, `useToggle` 4. **데이터 페칭 전략 선택** - 아래 판단 기준표를 참고하여 적합한 방식 결정 5. **구현** - 응답 타입을 반드시 명시: `useFetch()` - 상태와 메서드를 하나의 객체로 반환 - 부수효과 최소화 6. **검증** - TypeScript 오류 확인 - SSR 환경에서의 동작 고려 ## 데이터 페칭 판단 기준표 | 시나리오 | 추천 방식 | 이유 | | --- | --- | --- | | 단순 REST GET | `useFetch` | 자동 key 중복 방지, 간결함 | | 커스텀 key·transform 필요 | `useAsyncData` | 캐싱/변환 세밀 제어 | | 이벤트 핸들러 내 POST/PUT/DELETE | `$fetch` | SSR 불필요, fire-and-forget | | 의존 쿼리 (체이닝) | `useAsyncData` + `watch` | 실행 순서 제어 | | 서버 사이드 전용 로직 | `server/api/` + `$fetch` | Nitro 컨텍스트 | ## 데이터 페칭 Composable 템플릿 ```ts // composables/useUserProfile.ts import type { UserProfile } from '~/types/user'; export function useUserProfile(userId: Ref) { const { data, error, status, refresh } = useFetch( () => `/api/users/${userId.value}`, { key: `user-profile-${userId.value}`, watch: [userId], }, ); return { profile: data, error, isLoading: computed(() => status.value === 'pending'), refresh, }; } ``` ## useAsyncData 패턴 ```ts // composables/useProductList.ts import type { Product } from '~/types/product'; interface UseProductListOptions { category?: Ref; page?: Ref; } export function useProductList(options: UseProductListOptions = {}) { const { category, page } = options; const { data, error, status, refresh } = useAsyncData( 'product-list', () => $fetch('/api/products', { query: { category: category?.value, page: page?.value, }, }), { watch: [category, page].filter(Boolean), }, ); return { products: data, error, isLoading: computed(() => status.value === 'pending'), refresh, }; } ``` ## 로직 Composable 템플릿 ```ts // composables/useToggle.ts export function useToggle(initialValue = false) { const isOpen = ref(initialValue); function toggle() { isOpen.value = !isOpen.value; } function open() { isOpen.value = true; } function close() { isOpen.value = false; } return { isOpen, toggle, open, close }; } ``` ## 리스트 + 페이지네이션 패턴 ```ts // composables/usePaginatedList.ts interface UsePaginatedListOptions { url: string; pageSize?: number; } export function usePaginatedList(options: UsePaginatedListOptions) { const { url, pageSize = 20 } = options; const currentPage = ref(1); const { data, error, status, refresh } = useFetch<{ items: T[]; total: number; }>( () => url, { query: computed(() => ({ page: currentPage.value, size: pageSize, })), watch: [currentPage], }, ); const totalPages = computed(() => Math.ceil((data.value?.total ?? 0) / pageSize), ); function goToPage(page: number) { currentPage.value = page; } return { items: computed(() => data.value?.items ?? []), currentPage: readonly(currentPage), totalPages, error, isLoading: computed(() => status.value === 'pending'), goToPage, refresh, }; } ``` ## 주의사항 - **`$fetch`를 `