148 lines
4.3 KiB
Markdown
148 lines
4.3 KiB
Markdown
---
|
|
name: api-pinia-store
|
|
description: 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 템플릿
|
|
|
|
```ts
|
|
// 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 상태 스토어 템플릿
|
|
|
|
```ts
|
|
// 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 적용 패턴
|
|
|
|
```ts
|
|
// 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` 참조)
|