- CLAUDE.md 운영 규칙 - wiki/ 정리된 지식 페이지 (Nuxt + Claude Code) - raw/ 원본 자료 - reference/ Nuxt 4.x 공식 문서 Co-authored-by: Cursor <cursoragent@cursor.com>
237 lines
6.1 KiB
Markdown
237 lines
6.1 KiB
Markdown
# Nuxt 데이터 패칭
|
|
|
|
> **카테고리:** 핵심 개념
|
|
> **최종 수정:** 2026-05-13
|
|
> **관련:** [[nuxt-lifecycle]], [[nuxt-state-management]], [[nuxt-rendering-modes]]
|
|
|
|
## 요약
|
|
|
|
`$fetch`, `useFetch`, `useAsyncData` 세 가지가 있다. SSR 환경에서 이중 패칭을 막으려면 `useFetch` 또는 `useAsyncData`를 써야 한다. `$fetch`는 이벤트 핸들러(사용자 액션) 전용.
|
|
|
|
---
|
|
|
|
## 세 가지 비교
|
|
|
|
| | `$fetch` | `useFetch` | `useAsyncData` |
|
|
|---|---|---|---|
|
|
| SSR 중복 방지 | ❌ | ✅ | ✅ |
|
|
| 캐싱/재사용 | ❌ | ✅ (URL이 key) | ✅ (명시적 key) |
|
|
| 반응형 반환값 | ❌ | ✅ | ✅ |
|
|
| 사용 위치 | 어디서나 | setup 함수 내부 | setup 함수 내부 |
|
|
| 적합한 상황 | 폼 제출, 버튼 클릭 | 일반 데이터 로딩 | CMS/서드파티 쿼리 레이어 |
|
|
|
|
---
|
|
|
|
## `$fetch`
|
|
|
|
[ofetch](https://github.com/unjs/ofetch) 기반. 전역 자동 임포트.
|
|
|
|
```typescript
|
|
// 이벤트 핸들러에서 사용 (올바른 패턴)
|
|
async function submitForm() {
|
|
const result = await $fetch('/api/submit', {
|
|
method: 'POST',
|
|
body: { name: '홍길동' },
|
|
})
|
|
}
|
|
```
|
|
|
|
> ⚠️ 주의: `<script setup>`의 최상위 레벨에서 `$fetch`를 `await`하면 서버·클라이언트 양쪽에서 각각 실행되어 **이중 패칭** 발생. 반드시 `useFetch` 또는 `useAsyncData` 사용.
|
|
|
|
---
|
|
|
|
## `useFetch`
|
|
|
|
`useAsyncData + $fetch`의 편의 래퍼. URL 자체가 캐시 key.
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
// 기본 사용
|
|
const { data, status, error, refresh, execute, clear } = await useFetch('/api/posts')
|
|
|
|
// 타입 지정
|
|
const { data: posts } = await useFetch<Post[]>('/api/posts')
|
|
</script>
|
|
```
|
|
|
|
### 주요 반환값
|
|
|
|
| 값 | 설명 |
|
|
|---|---|
|
|
| `data` | 응답 데이터 (ref) |
|
|
| `status` | `'idle' \| 'pending' \| 'success' \| 'error'` |
|
|
| `error` | 에러 객체 (ref) |
|
|
| `refresh()` / `execute()` | 수동 재패칭 |
|
|
| `clear()` | data를 undefined로 초기화 |
|
|
|
|
---
|
|
|
|
## `useAsyncData`
|
|
|
|
CMS, 서드파티 쿼리 레이어, 복수 요청 묶기에 적합. 명시적 key 권장.
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
// 기본
|
|
const { data } = await useAsyncData('users', () => myGetFunction('users'))
|
|
|
|
// 동적 key (라우트 파라미터 활용)
|
|
const { id } = useRoute().params
|
|
const { data: user } = await useAsyncData(`user:${id}`, () => fetchUser(id))
|
|
|
|
// 복수 요청 병렬화
|
|
const { data } = await useAsyncData('cart', async (nuxtApp, { signal }) => {
|
|
const [coupons, offers] = await Promise.all([
|
|
$fetch('/cart/coupons', { signal }),
|
|
$fetch('/cart/offers', { signal }),
|
|
])
|
|
return { coupons, offers }
|
|
})
|
|
```
|
|
|
|
> ⚠️ 주의: `useAsyncData`는 데이터 패칭·캐싱 전용. Pinia 액션 호출 같은 사이드 이펙트에 쓰면 null 값으로 반복 실행됨 → `callOnce` 사용.
|
|
|
|
---
|
|
|
|
## 주요 옵션
|
|
|
|
### `lazy` — 네비게이션 차단 해제
|
|
|
|
기본값은 데이터 로딩 완료까지 페이지 전환 차단. `lazy: true`면 차단 해제, 수동 로딩 상태 처리 필요.
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
const { status, data: posts } = useFetch('/api/posts', { lazy: true })
|
|
// 또는 useLazyFetch('/api/posts')
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="status === 'pending'">로딩 중...</div>
|
|
<div v-else>{{ posts }}</div>
|
|
</template>
|
|
```
|
|
|
|
### `server: false` — 클라이언트 전용 패칭
|
|
|
|
```typescript
|
|
// hydration 완료 후에만 패칭
|
|
const { status, data } = useFetch('/api/comments', {
|
|
lazy: true,
|
|
server: false,
|
|
})
|
|
```
|
|
|
|
### `pick` / `transform` — 페이로드 크기 최소화
|
|
|
|
```typescript
|
|
// 필요한 필드만
|
|
const { data } = await useFetch('/api/mountains/everest', {
|
|
pick: ['title', 'description'],
|
|
})
|
|
|
|
// 변환
|
|
const { data } = await useFetch('/api/mountains', {
|
|
transform: (mountains) => mountains.map(m => ({ title: m.title })),
|
|
})
|
|
```
|
|
|
|
### `watch` — 반응형 재패칭
|
|
|
|
```typescript
|
|
const id = ref(1)
|
|
const { data } = await useFetch('/api/users', { watch: [id] })
|
|
// id 변경 시 자동 재패칭
|
|
```
|
|
|
|
### Computed URL — 동적 URL 재패칭
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
const id = ref(null)
|
|
|
|
// query 파라미터로 동적 URL
|
|
const { data } = useLazyFetch('/api/user', {
|
|
query: { user_id: id },
|
|
})
|
|
|
|
// URL 자체가 동적인 경우
|
|
const { data } = useLazyFetch(() => `/api/users/${id.value}`, {
|
|
immediate: false,
|
|
})
|
|
</script>
|
|
```
|
|
|
|
### `immediate: false` — 수동 실행
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
const { data, execute, status } = await useLazyFetch('/api/comments', {
|
|
immediate: false,
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<button v-if="status === 'idle'" @click="execute">데이터 불러오기</button>
|
|
<div v-else-if="status === 'pending'">로딩 중...</div>
|
|
<div v-else>{{ data }}</div>
|
|
</template>
|
|
```
|
|
|
|
---
|
|
|
|
## 캐싱과 키 공유
|
|
|
|
같은 key를 쓰는 컴포넌트는 동일한 `data`, `error`, `status` ref를 공유.
|
|
|
|
```typescript
|
|
// 같은 key → 동일 인스턴스 (옵션도 일관되어야 함)
|
|
const { data: users1 } = useAsyncData('users', fetchUsers, { deep: false })
|
|
const { data: users2 } = useAsyncData('users', fetchUsers, { deep: false }) // ✅
|
|
|
|
// 독립 인스턴스가 필요하면 다른 key 사용
|
|
const { data: users1 } = useAsyncData('users-1', fetchUsers)
|
|
const { data: users2 } = useAsyncData('users-2', fetchUsers)
|
|
```
|
|
|
|
반응형 key:
|
|
|
|
```typescript
|
|
const userId = ref('123')
|
|
const { data } = useAsyncData(
|
|
computed(() => `user-${userId.value}`),
|
|
() => fetchUser(userId.value),
|
|
)
|
|
// userId 변경 시 자동 재패칭 + 이전 캐시 정리
|
|
```
|
|
|
|
---
|
|
|
|
## 헤더·쿠키 전달
|
|
|
|
```typescript
|
|
// useFetch는 서버에서 useRequestFetch()로 클라이언트 헤더·쿠키 자동 프록시
|
|
const { data } = await useFetch('/api/echo')
|
|
|
|
// $fetch 직접 사용 시 수동으로 전달 필요
|
|
const headers = useRequestHeaders(['cookie'])
|
|
const data = await $fetch('/api/me', { headers })
|
|
```
|
|
|
|
---
|
|
|
|
## 자주 쓰는 유틸
|
|
|
|
| 유틸 | 용도 |
|
|
|---|---|
|
|
| `refreshNuxtData(key?)` | 특정 key 또는 전체 캐시 무효화 후 재패칭 |
|
|
| `clearNuxtData(key?)` | 캐시 데이터만 삭제 |
|
|
| `useNuxtData(key)` | 캐시된 데이터 읽기 전용 접근 |
|
|
|
|
---
|
|
|
|
## 참고 / 출처
|
|
|
|
- `reference/1.getting-started/10.data-fetching.md`
|
|
- `reference/4.api/2.composables/use-fetch.md`
|
|
- `reference/3.guide/5.recipes/3.custom-usefetch.md`
|