Files
nuxt-deep/docs/curriculum/04-data-fetching.md

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>