5.0 KiB
5.0 KiB
4. 데이터 패칭 (useAsyncData, useFetch 등)
핵심 개념
Nuxt의 데이터 패칭 컴포저블은 SSR과 CSR 양쪽을 처리한다.
- 서버에서 데이터를 가져와 HTML에 포함 → SEO 최적화, 빠른 초기 로딩
- 클라이언트로 상태 전달(payload) → Hydration 시 중복 요청 방지
- 캐시·로딩·에러 상태를 자동으로 관리
useFetch vs useAsyncData
useFetch |
useAsyncData |
|
|---|---|---|
| 용도 | URL 기반 데이터 패칭 | 모든 비동기 로직 |
| 내부 구현 | useAsyncData + $fetch |
직접 사용 |
| URL 변경 시 자동 재요청 | ✅ (watch 옵션) | 수동 설정 필요 |
| 권장 상황 | API URL이 명확할 때 | DB 쿼리, 복합 로직 |
useFetch 기본 사용법
<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>
옵션 활용
<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 기본 사용법
<script setup>
// 첫 번째 인자는 캐시 키 (고유해야 함)
const { data, pending, error } = await useAsyncData(
'products-list',
() => $fetch('/api/products')
)
</script>
Supabase와 함께 사용
<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 파라미터 변경 시 자동 재요청)
<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 중복 요청 안 함 ✅
서버/클라이언트 실행 분기
<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를 쓸 것)
<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>
로딩/에러 상태 처리 패턴
<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>