Initial implementation of the project structure and basic functionality.
This commit is contained in:
221
docs/curriculum/04-data-fetching.md
Normal file
221
docs/curriculum/04-data-fetching.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# 4. 데이터 패칭 (useAsyncData, useFetch 등)
|
||||
|
||||
## 핵심 개념
|
||||
|
||||
Nuxt의 데이터 패칭 컴포저블은 SSR과 CSR 양쪽을 처리한다.
|
||||
|
||||
- **서버에서 데이터를 가져와 HTML에 포함** → SEO 최적화, 빠른 초기 로딩
|
||||
- **클라이언트로 상태 전달(payload)** → Hydration 시 중복 요청 방지
|
||||
- **캐시·로딩·에러 상태**를 자동으로 관리
|
||||
|
||||
---
|
||||
|
||||
## useFetch vs useAsyncData
|
||||
|
||||
| | `useFetch` | `useAsyncData` |
|
||||
|--|-----------|----------------|
|
||||
| 용도 | URL 기반 데이터 패칭 | 모든 비동기 로직 |
|
||||
| 내부 구현 | `useAsyncData` + `$fetch` | 직접 사용 |
|
||||
| URL 변경 시 자동 재요청 | ✅ (watch 옵션) | 수동 설정 필요 |
|
||||
| 권장 상황 | API URL이 명확할 때 | DB 쿼리, 복합 로직 |
|
||||
|
||||
---
|
||||
|
||||
## useFetch 기본 사용법
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// GET /api/products 요청
|
||||
const { data, pending, error, refresh } = await useFetch('/api/products')
|
||||
// data: Ref<응답값>
|
||||
// pending: Ref<boolean> — 로딩 상태
|
||||
// error: Ref<Error | null>
|
||||
// refresh: () => Promise — 수동 재요청
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="pending">로딩 중...</div>
|
||||
<div v-else-if="error">에러: {{ error.message }}</div>
|
||||
<ul v-else>
|
||||
<li v-for="item in data" :key="item.id">{{ item.name }}</li>
|
||||
</ul>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 옵션 활용
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
const { data } = await useFetch('/api/products', {
|
||||
// 요청 파라미터 (반응형 지원)
|
||||
query: { category: 'tent', limit: 10 },
|
||||
|
||||
// 요청 헤더
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
|
||||
// 응답 데이터 변환
|
||||
transform: (response) => response.items,
|
||||
|
||||
// 기본값 (data가 null일 때)
|
||||
default: () => [],
|
||||
|
||||
// 서버에서만 실행 (클라이언트에서는 캐시 사용)
|
||||
server: true,
|
||||
|
||||
// 클라이언트에서만 실행
|
||||
// server: false,
|
||||
|
||||
// 컴포넌트 마운트 후 자동 실행 안 함
|
||||
lazy: true,
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## useAsyncData 기본 사용법
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// 첫 번째 인자는 캐시 키 (고유해야 함)
|
||||
const { data, pending, error } = await useAsyncData(
|
||||
'products-list',
|
||||
() => $fetch('/api/products')
|
||||
)
|
||||
</script>
|
||||
```
|
||||
|
||||
### Supabase와 함께 사용
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
const client = useSupabaseClient()
|
||||
|
||||
const { data: purchases } = await useAsyncData(
|
||||
'purchases',
|
||||
() => client
|
||||
.from('purchases')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false })
|
||||
.then(({ data }) => data)
|
||||
)
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 반응형 쿼리 (URL 파라미터 변경 시 자동 재요청)
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
const route = useRoute()
|
||||
|
||||
// route.params.id가 바뀌면 자동으로 재요청
|
||||
const { data: product } = await useFetch(
|
||||
() => `/api/products/${route.params.id}`
|
||||
)
|
||||
|
||||
// 또는 watch 옵션 사용
|
||||
const category = ref('tent')
|
||||
|
||||
const { data } = await useFetch('/api/products', {
|
||||
query: { category }, // category가 바뀌면 자동 재요청
|
||||
watch: [category],
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<select v-model="category">
|
||||
<option value="tent">텐트</option>
|
||||
<option value="chair">의자</option>
|
||||
</select>
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SSR 동작 원리
|
||||
|
||||
```
|
||||
1. [서버] useFetch('/api/products') 실행
|
||||
2. [서버] 데이터 가져옴: [{ id: 1, name: '텐트' }, ...]
|
||||
3. [서버] 데이터를 HTML에 포함 + payload에 직렬화
|
||||
4. [클라이언트] HTML 즉시 표시 (데이터가 이미 있음)
|
||||
5. [클라이언트] Hydration 시 payload에서 데이터 복원
|
||||
6. [클라이언트] /api/products 중복 요청 안 함 ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 서버/클라이언트 실행 분기
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// server: false → 클라이언트에서만 실행 (SEO 불필요한 데이터)
|
||||
const { data: userProfile } = await useFetch('/api/me', {
|
||||
server: false, // 서버에서 실행 안 함
|
||||
})
|
||||
|
||||
// lazy: true → 비동기적으로 로딩 (페이지 전환을 막지 않음)
|
||||
const { data: recommendations } = await useFetch('/api/recommendations', {
|
||||
lazy: true,
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## $fetch — 이벤트 핸들러에서 직접 요청
|
||||
|
||||
`$fetch`는 컴포저블 없이 즉시 요청할 때 사용한다.
|
||||
(setup() 최상위에서는 `useFetch`를 쓸 것)
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// 버튼 클릭 같은 이벤트에서 직접 호출
|
||||
async function handleSubmit() {
|
||||
const result = await $fetch('/api/purchases', {
|
||||
method: 'POST',
|
||||
body: { name: '텐트', price: 150000 }
|
||||
})
|
||||
console.log(result)
|
||||
}
|
||||
|
||||
// 삭제
|
||||
async function handleDelete(id) {
|
||||
await $fetch(`/api/purchases/${id}`, { method: 'DELETE' })
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 로딩/에러 상태 처리 패턴
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
const { data, pending, error, refresh } = await useFetch('/api/products', {
|
||||
default: () => []
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 로딩 중 -->
|
||||
<USkeletonList v-if="pending" />
|
||||
|
||||
<!-- 에러 -->
|
||||
<UAlert
|
||||
v-else-if="error"
|
||||
color="red"
|
||||
title="데이터를 불러오지 못했습니다"
|
||||
:description="error.message"
|
||||
>
|
||||
<template #actions>
|
||||
<UButton @click="refresh">다시 시도</UButton>
|
||||
</template>
|
||||
</UAlert>
|
||||
|
||||
<!-- 데이터 -->
|
||||
<ProductList v-else :items="data" />
|
||||
</template>
|
||||
```
|
||||
Reference in New Issue
Block a user