Files
fe-agent-new/skills/nuxt-composable/SKILL.md

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 상태/행동)을 구분하여 작성합니다.

작업 순서

  1. 관심사 식별

    • 데이터 페칭 composable인가? (API 호출, 서버 상태)
    • 로직 composable인가? (UI 상태, 재사용 행동)
  2. 기존 composable 탐색

    • composables/ 디렉토리에서 기존 패턴 확인
    • 중복 생성 방지
  3. 네이밍 결정

    • use 접두사 + PascalCase 도메인명
    • 파일: composables/useXxx.ts (Nuxt auto-import 대상)
    • 예: useAuth, useUserProfile, useProductList, useToggle
  4. 데이터 페칭 전략 선택

    • 아래 판단 기준표를 참고하여 적합한 방식 결정
  5. 구현

    • 응답 타입을 반드시 명시: useFetch<ResponseType>()
    • 상태와 메서드를 하나의 객체로 반환
    • 부수효과 최소화
  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 템플릿

// 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가 담당