| name | nuxt4-patterns |
| description | hydration 안전성, 성능, route rules, lazy loading, `useFetch`와 `useAsyncData`를 활용한 SSR 안전 데이터 패칭을 다루는 Nuxt 4 앱 패턴입니다. |
| origin | ECC |
Nuxt 4 패턴
SSR, 하이브리드 렌더링, route rules, 페이지 단위 데이터 패칭이 포함된 Nuxt 4 앱을 만들거나 디버깅할 때 사용합니다.
활성화 시점
- 서버 HTML과 클라이언트 상태 사이 hydration mismatch가 날 때
- prerender, SWR, ISR, client-only 섹션 같은 라우트 단위 렌더링 결정을 다룰 때
- lazy loading, lazy hydration, payload 크기 관련 성능 작업을 할 때
useFetch, useAsyncData, $fetch로 페이지/컴포넌트 데이터를 가져올 때
- route params, middleware, SSR/클라이언트 차이와 얽힌 Nuxt 라우팅 문제를 다룰 때
Hydration 안전성
- 첫 렌더는 결정론적으로 유지합니다.
Date.now(), Math.random(), 브라우저 전용 API, 저장소 읽기를 SSR 템플릿 상태에 직접 넣지 않습니다.
- 서버가 같은 마크업을 만들 수 없으면 브라우저 전용 로직을
onMounted(), import.meta.client, ClientOnly, .client.vue 뒤로 옮깁니다.
vue-router의 것이 아니라 Nuxt의 useRoute() composable을 사용합니다.
route.fullPath로 SSR 렌더링 마크업을 결정하지 않습니다. URL fragment는 클라이언트 전용이라 hydration mismatch를 만들 수 있습니다.
ssr: false는 진짜 브라우저 전용 영역에만 쓰는 탈출구입니다. mismatch의 기본 해결책처럼 쓰지 않습니다.
데이터 패칭
- 페이지와 컴포넌트의 SSR 안전 API 읽기에는
await useFetch()를 우선합니다. 서버에서 가져온 데이터를 Nuxt payload로 전달해 hydration 시 2차 fetch를 피합니다.
- fetcher가 단순
$fetch()가 아니거나, 커스텀 키가 필요하거나, 여러 비동기 소스를 조합할 때는 useAsyncData()를 사용합니다.
useAsyncData()에는 캐시 재사용과 예측 가능한 갱신을 위해 안정적인 키를 부여합니다.
useAsyncData() 핸들러는 부수효과 없이 유지합니다. SSR과 hydration 중 모두 실행될 수 있습니다.
- 사용자 트리거 쓰기 작업이나 클라이언트 전용 동작에는
$fetch()를 사용하고, SSR에서 수화돼야 하는 상단 페이지 데이터에는 쓰지 않습니다.
- 내비게이션을 막지 않아야 하는 비핵심 데이터에는
lazy: true, useLazyFetch(), useLazyAsyncData()를 사용하고 UI에서 status === 'pending'를 처리합니다.
- SEO나 첫 페인트에 필요 없는 데이터에만
server: false를 사용합니다.
pick으로 payload를 줄이고, 깊은 반응성이 불필요하면 얕은 payload를 선호합니다.
const route = useRoute()
const { data: article, status, error, refresh } = await useAsyncData(
() => `article:${route.params.slug}`,
() => $fetch(`/api/articles/${route.params.slug}`),
)
const { data: comments } = await useFetch(`/api/articles/${route.params.slug}/comments`, {
lazy: true,
server: false,
})
Route Rules
렌더링과 캐싱 전략은 nuxt.config.ts의 routeRules를 우선합니다.
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true },
'/products/**': { swr: 3600 },
'/blog/**': { isr: true },
'/admin/**': { ssr: false },
'/api/**': { cache: { maxAge: 60 * 60 } },
},
})
prerender: 빌드 시 정적 HTML 생성
swr: 캐시된 콘텐츠를 제공하고 백그라운드에서 재검증
isr: 지원 플랫폼에서 incremental static regeneration
ssr: false: 클라이언트 렌더링 라우트
cache / redirect: Nitro 레벨 응답 동작
route group별로 규칙을 고릅니다. 마케팅 페이지, 카탈로그, 대시보드, API는 대개 서로 다른 전략이 필요합니다.
Lazy Loading 및 성능
- Nuxt는 기본적으로 라우트별 페이지 코드를 분할합니다. 마이크로 최적화 전에 라우트 경계를 의미 있게 유지합니다.
- 비핵심 컴포넌트는
Lazy 접두사로 동적 import합니다.
- UI가 실제로 필요로 할 때만 청크가 로드되도록
v-if로 조건부 렌더링합니다.
- 화면 아래쪽 또는 비핵심 인터랙션 UI에는 lazy hydration을 사용합니다.
<template>
<LazyRecommendations v-if="showRecommendations" />
<LazyProductGallery hydrate-on-visible />
</template>
- 커스텀 전략이 필요하면
defineLazyHydrationComponent()와 visibility/idle 전략을 사용합니다.
- Nuxt lazy hydration은 단일 파일 컴포넌트에 적용됩니다. lazily hydrated 컴포넌트에 새 props를 넘기면 즉시 hydration이 트리거됩니다.
- 내부 이동은
NuxtLink를 사용해 Nuxt가 라우트 컴포넌트와 생성된 payload를 prefetch할 수 있게 합니다.
리뷰 체크리스트
- 첫 SSR 렌더와 hydration된 클라이언트 렌더가 같은 마크업을 만든다
- 페이지 데이터는 상단
$fetch가 아니라 useFetch 또는 useAsyncData를 사용한다
- 비핵심 데이터는 lazy 처리되고 명시적 로딩 UI가 있다
- route rules가 페이지의 SEO와 신선도 요구에 맞는다
- 무거운 인터랙티브 섬은 lazy load 또는 lazy hydration된다