151 lines
3.7 KiB
Markdown
151 lines
3.7 KiB
Markdown
---
|
|
name: dev-api-state
|
|
description: |
|
|
API 연동 패턴(useFetch / useAsyncData / $fetch)과 Pinia 상태관리 코드를
|
|
상황에 맞게 자동 선택·생성합니다. BFF 패턴, 에러 핸들링, 로딩 상태를 포함합니다.
|
|
|
|
다음 상황에서 반드시 사용하세요:
|
|
- "이 API 연동 + 스토어 만들어줘", "Pinia store 작성해줘"
|
|
- "API 데이터 페칭 어떻게 해?", "useFetch vs useAsyncData 언제 써?"
|
|
- API 키 보호를 위한 BFF(server/api/) 패턴이 필요할 때
|
|
---
|
|
|
|
# API 연동 & 상태관리 (dev-api-state)
|
|
|
|
API 스펙 → useFetch/useAsyncData 패턴 선택 → Pinia Setup Store 코드 자동 생성.
|
|
|
|
## 언제 사용하는가
|
|
|
|
- 새 API 연동 코드와 Pinia 스토어를 함께 작성할 때
|
|
- `useFetch` / `$fetch` / `useAsyncData` 중 적절한 패턴을 결정할 때
|
|
- API 키를 클라이언트에 노출하지 않기 위한 BFF 패턴이 필요할 때
|
|
|
|
## 데이터 페칭 패턴 선택 기준
|
|
|
|
| 상황 | 권장 패턴 |
|
|
|---|---|
|
|
| SSR 페이지 초기 데이터 | `useAsyncData` + `$fetch` |
|
|
| 컴포넌트 마운트 후 데이터 | `useFetch` |
|
|
| 사용자 액션으로 트리거 | `$fetch` (직접 호출) |
|
|
| API 키 보호 필요 | `server/api/` BFF + `useFetch` |
|
|
| 복잡한 캐싱/의존성 | `useAsyncData` with key |
|
|
|
|
---
|
|
|
|
## 작업 순서
|
|
|
|
### Phase 1: API 스펙 파악
|
|
|
|
1. 엔드포인트, 요청/응답 타입을 파악한다.
|
|
2. 아래를 확인한다:
|
|
- 인증 헤더 필요 여부
|
|
- API 키 노출 위험 여부 → BFF 패턴 적용
|
|
- 캐시 전략 (항상 최신 vs TTL)
|
|
|
|
### Phase 2: 타입 정의
|
|
|
|
```ts
|
|
// types/product.ts
|
|
export interface Product {
|
|
id: string
|
|
name: string
|
|
price: number
|
|
imageUrl: string
|
|
}
|
|
|
|
export interface ProductListResponse {
|
|
data: Product[]
|
|
total: number
|
|
page: number
|
|
}
|
|
```
|
|
|
|
### Phase 3: BFF 레이어 (필요 시)
|
|
|
|
API 키 보호가 필요한 경우 server/api/ 경유:
|
|
|
|
```ts
|
|
// server/api/products/index.get.ts
|
|
export default defineEventHandler(async (event) => {
|
|
const query = getQuery(event)
|
|
const response = await $fetch<ProductListResponse>(
|
|
`${process.env.API_BASE}/products`,
|
|
{
|
|
headers: { 'x-api-key': process.env.API_KEY! },
|
|
query,
|
|
}
|
|
)
|
|
return response
|
|
})
|
|
```
|
|
|
|
### Phase 4: Pinia Setup Store 생성
|
|
|
|
Setup Store 패턴을 기본으로 사용한다:
|
|
|
|
```ts
|
|
// stores/product.ts
|
|
import type { Product } from '~/types/product'
|
|
|
|
export const useProductStore = defineStore('product', () => {
|
|
const items = ref<Product[]>([])
|
|
const isLoading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
|
|
const fetchProducts = async (page = 1) => {
|
|
isLoading.value = true
|
|
error.value = null
|
|
try {
|
|
const { data } = await useFetch('/api/products', { query: { page } })
|
|
items.value = data.value?.data ?? []
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : '알 수 없는 오류'
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
return { items, isLoading, error, fetchProducts }
|
|
})
|
|
```
|
|
|
|
### Phase 5: 컴포넌트 연결 예시
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
const productStore = useProductStore()
|
|
const { items, isLoading, error } = storeToRefs(productStore)
|
|
|
|
onMounted(() => productStore.fetchProducts())
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="isLoading" aria-live="polite">로딩 중...</div>
|
|
<div v-else-if="error" role="alert">{{ error }}</div>
|
|
<ul v-else>
|
|
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
|
|
</ul>
|
|
</template>
|
|
```
|
|
|
|
---
|
|
|
|
## 출력 형식
|
|
|
|
```
|
|
## API 연동: <기능명>
|
|
|
|
### 선택된 패턴
|
|
- 페칭: useFetch | useAsyncData | $fetch
|
|
- BFF: 사용 | 미사용
|
|
- 이유: [선택 근거]
|
|
|
|
### 파일 목록
|
|
- `types/<domain>.ts`
|
|
- `server/api/...` (BFF 사용 시)
|
|
- `stores/<domain>.ts`
|
|
|
|
### 코드
|
|
[타입 → BFF → 스토어 → 컴포넌트 순]
|
|
```
|