Files
fe-agent-new/skills/api-pinia-store/SKILL.md

4.3 KiB

name, description
name description
api-pinia-store Pinia 스토어를 팀 컨벤션에 맞게 생성할 때 사용합니다. "스토어 만들어줘", "Pinia 스토어", "전역 상태", "store 추가", "상태관리", "useAuthStore", "useCartStore" 등을 요청하면 트리거됩니다.

Pinia 스토어 생성

이 skill은 Pinia 스토어를 팀 컨벤션에 맞게 생성합니다. Setup Store(Composition API) 문법을 기본으로 사용합니다.

작업 순서

  1. 스토어 필요성 판단

    • 아래 판단 가이드를 참고하여 스토어가 적합한지 확인
    • 서버 데이터 → composable(useFetch), 로컬 상태 → ref/reactive 권장
  2. 기존 스토어 탐색

    • stores/ 디렉토리에서 기존 스토어 패턴 확인
    • 중복 생성 방지, 기존 스토어 확장 가능 여부 검토
  3. 상태 설계

    • TypeScript 인터페이스로 상태 형태 정의
    • 최소한의 원시 상태만 저장, 파생 가능한 값은 computed
  4. Setup Store 구현

    • state → ref/reactive
    • getters → computed
    • actions → 일반 함수
    • 모두 하나의 객체로 반환
  5. persistence 추가 (필요 시)

    • pinia-plugin-persistedstate 사용 여부 사용자 확인
  6. 검증

    • TypeScript 오류 확인
    • SSR hydration 호환성 확인

스토어 필요성 판단 가이드

상태의 성격 해결 방법 예시
서버 데이터 (API 응답) composable + useFetch 상품 목록, 유저 프로필
앱 전역 공유 상태 Pinia 스토어 인증, 테마, 사이드바 상태
단일 페이지 폼 상태 로컬 ref/reactive 회원가입 폼
부모↔자식 공유 상태 props/emits 또는 provide/inject 아코디언 그룹
URL 기반 상태 라우트 쿼리/파라미터 필터, 정렬, 페이지

Setup Store 템플릿

// stores/useAuthStore.ts
import type { User } from '~/types/user';

export const useAuthStore = defineStore('auth', () => {
  // state
  const user = ref<User | null>(null);
  const token = ref<string | null>(null);

  // getters
  const isLoggedIn = computed(() => !!token.value);
  const userName = computed(() => user.value?.name ?? '');

  // actions
  async function login(credentials: LoginCredentials) {
    const response = await $fetch<AuthResponse>('/api/auth/login', {
      method: 'POST',
      body: credentials,
    });
    user.value = response.user;
    token.value = response.token;
  }

  function logout() {
    user.value = null;
    token.value = null;
    navigateTo('/login');
  }

  return {
    // state
    user,
    token,
    // getters
    isLoggedIn,
    userName,
    // actions
    login,
    logout,
  };
});

UI 상태 스토어 템플릿

// stores/useUiStore.ts
export const useUiStore = defineStore('ui', () => {
  const isSidebarOpen = ref(false);
  const theme = ref<'light' | 'dark'>('light');

  function toggleSidebar() {
    isSidebarOpen.value = !isSidebarOpen.value;
  }

  function setTheme(newTheme: 'light' | 'dark') {
    theme.value = newTheme;
  }

  return {
    isSidebarOpen,
    theme,
    toggleSidebar,
    setTheme,
  };
});

Persistence 적용 패턴

// stores/useAuthStore.ts
export const useAuthStore = defineStore('auth', () => {
  const token = ref<string | null>(null);
  // ... 생략
  return { token };
}, {
  persist: {
    pick: ['token'],
  },
});

스토어 네이밍 규칙

  • 파일: stores/use[Domain]Store.ts
  • 함수: use[Domain]Store
  • defineStore ID: 소문자 도메인명 (예: 'auth', 'cart', 'ui')

주의사항

  • Option Store 대신 Setup Store 사용: <script setup> 패턴과의 일관성
  • 서버 데이터를 Pinia에 저장하지 않음: useFetch/useAsyncData 영역
  • SSR hydration: 스토어 상태는 HTML로 직렬화됨. 함수, DOM 참조, 클래스 인스턴스 등 직렬화 불가능한 값 저장 금지
  • 스토어 간 순환 참조 금지: A → B → A 형태의 의존성 제거
  • 대형 모놀리식 스토어 지양: 도메인별 분리 (auth, ui, cart 등)
  • Action 내 직접 API 호출 주의: 프로젝트 규칙에 따라 composable에서 호출 후 결과만 저장하는 패턴 확인
  • any 타입 사용 금지 (rules/coding-conventions.md 참조)