5.1 KiB
5.1 KiB
name, description
| name | description |
|---|---|
| nuxt-composable | Nuxt 3 composable(useXxx)과 데이터 페칭(useFetch/useAsyncData) 패턴을 작성할 때 사용합니다. "composable 만들어줘", "useFetch 패턴", "데이터 페칭", "useAsyncData", "커스텀 훅", "로직 추출" 등을 요청하면 트리거됩니다. |
Nuxt Composable · 데이터 페칭
이 skill은 Nuxt 3의 composable 함수와 데이터 페칭 패턴을 팀 컨벤션에 맞게 생성합니다. 데이터 페칭 composable(서버 상태)과 로직 composable(UI 상태/행동)을 구분하여 작성합니다.
작업 순서
-
관심사 식별
- 데이터 페칭 composable인가? (API 호출, 서버 상태)
- 로직 composable인가? (UI 상태, 재사용 행동)
-
기존 composable 탐색
composables/디렉토리에서 기존 패턴 확인- 중복 생성 방지
-
네이밍 결정
use접두사 + PascalCase 도메인명- 파일:
composables/useXxx.ts(Nuxt auto-import 대상) - 예:
useAuth,useUserProfile,useProductList,useToggle
-
데이터 페칭 전략 선택
- 아래 판단 기준표를 참고하여 적합한 방식 결정
-
구현
- 응답 타입을 반드시 명시:
useFetch<ResponseType>() - 상태와 메서드를 하나의 객체로 반환
- 부수효과 최소화
- 응답 타입을 반드시 명시:
-
검증
- TypeScript 오류 확인
- SSR 환경에서의 동작 고려
데이터 페칭 판단 기준표
| 시나리오 | 추천 방식 | 이유 |
|---|---|---|
| 단순 REST GET | useFetch |
자동 key 중복 방지, 간결함 |
| 커스텀 key·transform 필요 | useAsyncData |
캐싱/변환 세밀 제어 |
| 이벤트 핸들러 내 POST/PUT/DELETE | $fetch |
SSR 불필요, fire-and-forget |
| 의존 쿼리 (체이닝) | useAsyncData + watch |
실행 순서 제어 |
| 서버 사이드 전용 로직 | server/api/ + $fetch |
Nitro 컨텍스트 |
데이터 페칭 Composable 템플릿
// composables/useUserProfile.ts
import type { UserProfile } from '~/types/user';
export function useUserProfile(userId: Ref<string>) {
const { data, error, status, refresh } = useFetch<UserProfile>(
() => `/api/users/${userId.value}`,
{
key: `user-profile-${userId.value}`,
watch: [userId],
},
);
return {
profile: data,
error,
isLoading: computed(() => status.value === 'pending'),
refresh,
};
}
useAsyncData 패턴
// composables/useProductList.ts
import type { Product } from '~/types/product';
interface UseProductListOptions {
category?: Ref<string>;
page?: Ref<number>;
}
export function useProductList(options: UseProductListOptions = {}) {
const { category, page } = options;
const { data, error, status, refresh } = useAsyncData(
'product-list',
() => $fetch<Product[]>('/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 템플릿
// 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 };
}
리스트 + 페이지네이션 패턴
// composables/usePaginatedList.ts
interface UsePaginatedListOptions<T> {
url: string;
pageSize?: number;
}
export function usePaginatedList<T>(options: UsePaginatedListOptions<T>) {
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를<script setup>에서 직접 사용 금지: SSR 시 서버/클라이언트 양쪽에서 실행되어 이중 요청 발생. 반드시useFetch또는useAsyncData로 감싸야 함- composable 파일은
composables/디렉토리에use접두사로 작성 (Nuxt auto-import) - 반환값은 개별 ref가 아닌 객체로 묶어서 반환. 소비자가 구조 분해 결정
- 부수효과(side effect)를 최소화하고, 상태와 메서드를 함께 반환
- 응답 타입 반드시 명시:
useFetch<T>(),useAsyncData<T>() any타입 사용 금지 (rules/coding-conventions.md참조)- 서버 상태는 Pinia에 넣지 않음.
useFetch/useAsyncData가 담당