티스토리 뷰

FrontEnd/nuxt

nuxt 에러 핸들링

til-odin 2025. 5. 7. 07:09

00. TL;DR

Nuxt 3에서 데이터 패치와 컴포넌트 렌더링 중 발생할 수 있는 오류에 대해 3가지 대응 방식을 설명합니다.

  1. 페이지 레벨 데이터 패치 오류 – SSR/CSR 전역 처리 (useAsyncData + error.vue)
  2. 컴포넌트 단위 데이터 패치 오류 – CSR 대응 (useFetch + ErrorBoundary)
  3. 일반 렌더링 로직 오류 – 런타임 예외 대응 (ErrorBoundary)

원칙:

  • 페이지 단의 오류는 Nuxt의 에러 핸들링 시스템과 layouts/error.vue를 통해 처리
  • 컴포넌트 레벨 오류는 <NuxtErrorBoundary>로 로컬하게 처리
  • 로딩은 각 컴포넌트 내부에서 ref()로 상태 관리하며 <Loader> 컴포넌트로 UI 분리

01. Nuxt의 전체 오류 처리 체계

처리 범위 위치 처리 방법 SSR/CSR 대응 UI 처리
페이지 초기 데이터 패치 실패 useAsyncData throw createError() SSR/CSR 모두 layouts/error.vue
컴포넌트 내부 fetch 실패 useFetch throw CSR 중심 <NuxtErrorBoundary>
일반 렌더링/로직 오류 모든 컴포넌트 런타임 throw CSR <NuxtErrorBoundary>

02. 페이지 단위 오류 처리 구조

02.01. 코드 예시 – useAsyncData 오류 처리

<script lang="ts" setup>
const { data: pageData, error } = await useAsyncData<{ title: string }>('pageData', () =>
  $fetch('/api/page')
)

if (error.value) {
  throw createError({
    statusCode: error.value.statusCode ?? 500,
    statusMessage: '페이지 데이터를 불러올 수 없습니다'
  })
}
</script>

<template>
  <div>
    <h1>{{ pageData.title }}</h1>
    <ErrorBoundary><UserCard /></ErrorBoundary>
  </div>
</template>

02.02. 전역 에러 페이지 – layouts/error.vue

<template>
  <div class="p-6 text-center text-red-700">
    <h1 class="text-2xl font-bold">에러 발생</h1>
    <p class="mt-2">상태코드: {{ error.statusCode }}</p>
    <p>{{ error.message }}</p>
    <button @click="navigateTo('/')">홈으로</button>
  </div>
</template>

<script lang="ts" setup>
defineProps<{ error: Error & { statusCode?: number } }>()
</script>

03. 컴포넌트 단위 오류 처리 구조

03.01. UserCard.vue

<script lang="ts" setup>
const user = ref<{ name: string }>()
const loading = ref(true)

const loadUser = async () => {
  loading.value = true
  try {
    const { data } = await useFetch<{ name: string }>('/api/user', {
      onFetchError({ error }) {
        throw error
      }
    })
    if (!data.value?.name) throw new Error('이름 정보 없음')
    user.value = data.value
  } finally {
    loading.value = false
  }
}

defineExpose({ retry: loadUser })
onMounted(loadUser)
</script>

<template>
  <Loader :loading="loading">
    <p>{{ user?.name }}</p>
  </Loader>
</template>

03.02. ErrorBoundary.vue

<template>
  <NuxtErrorBoundary>
    <slot :expose="exposeChild" />
    <template #error="{ error, clearError }">
      <div class="bg-red-100 p-4 text-red-700 border rounded">
        <p>{{ error.message }}</p>
        <button @click="() => { clearError(); childRef?.retry?.() }">재시도</button>
      </div>
    </template>
  </NuxtErrorBoundary>
</template>

<script lang="ts" setup>
const childRef = ref<any>()
const exposeChild = (el: unknown) => (childRef.value = el)
</script>

03.03. Loader.vue

<template>
  <slot v-if="!loading" />
  <slot name="loading" v-else>
    <div class="text-gray-500">로딩 중...</div>
  </slot>
</template>

<script lang="ts" setup>
defineProps<{ loading: boolean }>()
</script>

💡 커스텀 로더 예시

<Loader :loading="isLoading">
  <template #loading>
    <div class="flex items-center justify-center py-4">
      <Icon name="lucide:loader" class="animate-spin mr-2" />
      <span>불러오는 중입니다...</span>
    </div>
  </template>

  <p>콘텐츠 로딩 완료!</p>
</Loader>

04. 일반 렌더링 로직 오류 대응 구조

04.01. ProductPrice.vue

<script lang="ts" setup>
const props = defineProps<{ price: number }>()
const formattedPrice = `$${props.price.toFixed(2)}`
</script>

<template>
  <p>{{ formattedPrice }}</p>
</template>

05. 전체 페이지 구성 예시

<template>
  <h1>{{ pageData.title }}</h1>

  <ErrorBoundary>
    <UserCard />
  </ErrorBoundary>

  <ErrorBoundary>
    <ProductList />
  </ErrorBoundary>

  <ErrorBoundary>
    <ProductPrice :price="undefined" />
  </ErrorBoundary>
</template>

<script lang="ts" setup>
import ErrorBoundary from '@/components/common/ErrorBoundary.vue'
import Loader from '@/components/common/Loader.vue'
import UserCard from '@/components/UserCard.vue'
import ProductList from '@/components/ProductList.vue'
import ProductPrice from '@/components/ProductPrice.vue'

const { data: pageData, error } = await useAsyncData<{ title: string }>('pageData', () =>
  $fetch('/api/page')
)

if (error.value) {
  throw createError({ statusCode: 500, statusMessage: '페이지 데이터 로딩 실패' })
}
</script>

06. 전체 흐름 요약

레벨 API 호출 오류 발생 시 로딩 표시 책임 분리
페이지 (useAsyncData) SSR + CSR createError()error.vue v-if="!data" 등 직접 처리 최상단
컴포넌트 (useFetch) CSR throwErrorBoundary 내부 loading + Loader 각 컴포넌트 단위
일반 렌더링 없음 런타임 오류 → ErrorBoundary 없음 각 컴포넌트 단위

07. 결론

  • 페이지 단위의 SSR/CSR 오류는 useAsyncData + createError()layouts/error.vue로 대응합니다.
  • 컴포넌트 단위 fetch 오류와 렌더링 오류는 <ErrorBoundary>로 격리 처리합니다.
  • 로딩은 컴포넌트 내부에서 관리하고 <Loader> 컴포넌트로 분리하며, 필요 시 #loading 슬롯으로 UI 커스터마이징이 가능합니다.
  • 재시도는 defineExpose({ retry })를 통해 상위 컴포넌트와 인터페이스를 연결합니다.
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함