Nuxt 3: SSR과 정적 생성 완벽 가이드
Nuxt 3로 SSR과 정적 생성을 마스터하십시오. useFetch부터 route rules까지, Vue.js 애플리케이션의 성능을 최적화하는 방법을 배웁니다.

Nuxt 3은 다양한 사용 사례에 적합한 여러 렌더링 모드를 제공함으로써 Vue.js 애플리케이션을 구축하는 방식을 바꿔놓습니다. Server-Side Rendering(SSR)부터 정적 생성, 하이브리드 렌더링까지 이 프레임워크는 성능과 SEO를 최적화하기 위한 뛰어난 유연성을 제공합니다.
이 튜토리얼은 Vue 3과 Composition API에 대한 기본 지식을 전제로 합니다. 서버 사이드 렌더링 개념에 익숙하면 도움이 되지만 필수는 아니며, 기본 개념은 가이드 전반에 걸쳐 설명합니다.
Nuxt 3의 렌더링 모드 이해하기
코드를 살펴보기 전에 사용할 수 있는 렌더링 모드 간의 차이를 이해하는 것이 매우 중요합니다. 각 모드는 성능, SEO, 사용자 경험 측면에서 특정한 요구를 충족합니다.
SSR(Server-Side Rendering)은 모든 요청에 대해 서버에서 HTML을 생성합니다. 정적 생성(SSG)은 빌드 시점에 모든 페이지를 미리 생성합니다. 하이브리드 모드는 페이지 단위로 이러한 접근 방식을 조합할 수 있게 해줍니다.
// 다양한 렌더링 모드의 설정
export default defineNuxtConfig({
// SSR 기본 활성화 (SEO를 위해 권장)
ssr: true,
// 정적 생성: 모든 페이지를 사전 렌더링
// 빌드에는 'npm run generate' 사용
// target: 'static', // Nuxt 2 문법
// 하이브리드 모드: 라우트 단위로 설정 가능
routeRules: {
// 메인 페이지: 사전 렌더링 및 캐시
'/': { prerender: true },
// 블로그: 정적 생성
'/blog/**': { prerender: true },
// 대시보드: 클라이언트 사이드 렌더링만 사용
'/dashboard/**': { ssr: false },
// API: 사전 렌더링 없음
'/api/**': { prerender: false }
}
})이 설정은 하이브리드 모드의 강력함을 보여줍니다. 애플리케이션의 각 영역이 자신의 요구에 가장 적합한 렌더링 모드를 사용합니다.
useFetch와 useAsyncData를 활용한 데이터 가져오기
Nuxt 3은 데이터를 동형(isomorphic)으로 가져오기 위한 두 가지 주요 컴포저블을 제공합니다. 이 컴포저블들은 서버 측과 클라이언트 측 모두에서 동작하며 하이드레이션을 자동으로 관리합니다.
useFetch는 HTTP 호출을 단순화하는 useAsyncData의 래퍼입니다. useAsyncData는 고급 사용 사례에서 더 많은 제어를 제공합니다.
<script setup lang="ts">
// pages/blog/[slug].vue
// useFetch를 사용한 기사 상세 페이지
// 라우트 파라미터 가져오기
const route = useRoute()
// useFetch: 자동 데이터 가져오기
// 데이터는 서버 측에서 가져온 뒤 클라이언트에서 하이드레이트됩니다
const { data: article, pending, error } = await useFetch(
`/api/articles/${route.params.slug}`,
{
// 캐시 및 중복 제거를 위한 고유 키
key: `article-${route.params.slug}`,
// 필요 시 데이터 변환
transform: (response) => response.data,
// 캐시 옵션
getCachedData: (key) => {
// 데이터가 캐시에 있는지 확인
const nuxtApp = useNuxtApp()
return nuxtApp.payload.data[key]
}
}
)
// 네비게이션을 동반한 오류 처리
if (error.value) {
throw createError({
statusCode: 404,
message: '기사를 찾을 수 없습니다'
})
}
</script>
<template>
<div>
<div v-if="pending" class="loading">
기사를 불러오는 중...
</div>
<article v-else-if="article">
<h1>{{ article.title }}</h1>
<div v-html="article.content" />
</article>
</div>
</template>더 많은 제어가 필요한 경우 useAsyncData를 사용하면 임의의 비동기 함수를 실행할 수 있습니다.
<script setup lang="ts">
// pages/products/index.vue
// useAsyncData와 필터를 활용한 상품 목록
const route = useRoute()
// useAsyncData: 페칭 로직에 대한 완전한 제어
const { data: products, refresh } = await useAsyncData(
'products-list',
async () => {
// 필요하면 여러 소스에서 가져오기
const [productsResponse, categoriesResponse] = await Promise.all([
$fetch('/api/products', {
query: {
category: route.query.category,
sort: route.query.sort || 'date'
}
}),
$fetch('/api/categories')
])
// 데이터 결합 및 변환
return {
products: productsResponse.data,
categories: categoriesResponse.data,
total: productsResponse.meta.total
}
},
{
// 쿼리 파라미터가 변경되면 다시 가져오기
watch: [() => route.query]
}
)
// 수동 갱신 함수
const updateFilters = async (newCategory: string) => {
await navigateTo({
query: { ...route.query, category: newCategory }
})
}
</script>이 컴포저블들은 이중 페칭을 방지합니다. 서버에서 가져온 데이터는 HTML 페이로드에 직렬화되어 클라이언트 하이드레이션 시 재사용됩니다.
서버 훅으로 SSR 커스터마이즈하기
Nuxt 3의 SSR은 서버 훅을 통해 커스터마이즈할 수 있습니다. 이러한 훅은 렌더링 사이클의 다양한 단계에 개입하여 기본 동작을 수정할 수 있게 해줍니다.
// SSR 렌더링을 커스터마이즈하기 위한 서버 플러그인
export default defineNitroPlugin((nitroApp) => {
// 각 페이지 렌더링 전에 실행되는 훅
nitroApp.hooks.hook('render:html', (html, { event }) => {
// 스크립트나 메타데이터 주입
html.head.push(`
<script>
// 분석 또는 글로벌 설정
window.__APP_CONFIG__ = {
environment: '${process.env.NODE_ENV}',
apiUrl: '${process.env.API_URL}'
}
</script>
`)
})
// 렌더링 캐시 관리를 위한 훅
nitroApp.hooks.hook('render:response', (response, { event }) => {
// 사용자 정의 캐시 헤더 추가
const path = event.path
if (path.startsWith('/blog/')) {
// 블로그 기사에는 긴 캐시
response.headers['Cache-Control'] = 'public, max-age=3600, s-maxage=86400'
} else if (path.startsWith('/api/')) {
// API에는 캐시 사용 안 함
response.headers['Cache-Control'] = 'no-store'
}
})
})render:response 훅은 HTTP 캐시 전략을 구현하기에 이상적입니다. SSR을 Cache-Control 헤더를 존중하는 CDN과 결합하면 사전 렌더링된 페이지를 제공하면서 무효화 능력을 유지할 수 있습니다.
nuxt generate를 활용한 정적 생성
정적 생성은 빌드 시점에 모든 페이지를 미리 빌드합니다. 이 접근 방식은 블로그, 문서, 마케팅 사이트와 같이 콘텐츠가 안정적인 사이트에 이상적입니다.
동적 라우트의 경우 Nuxt가 생성해야 하는 모든 URL을 알아야 합니다. prerender:routes 훅을 통해 이러한 라우트를 프로그래밍 방식으로 정의할 수 있습니다.
// 정적 생성을 위한 완전한 설정
export default defineNuxtConfig({
// 정적 생성 활성화
nitro: {
prerender: {
// 자동 링크 크롤링 활성화
crawlLinks: true,
// 항상 포함할 라우트
routes: ['/', '/about', '/contact'],
// 일부 라우트 무시
ignore: ['/admin', '/api']
}
},
hooks: {
// 동적 라우트를 생성하기 위한 훅
async 'prerender:routes'(ctx) {
// API 또는 DB에서 기사 가져오기
const articles = await fetch('https://api.example.com/articles')
.then(res => res.json())
// 기사 라우트 추가
for (const article of articles) {
ctx.routes.add(`/blog/${article.slug}`)
}
// 카테고리 가져오기
const categories = await fetch('https://api.example.com/categories')
.then(res => res.json())
for (const category of categories) {
ctx.routes.add(`/category/${category.slug}`)
}
}
}
})페이지 수가 많은 프로젝트의 경우 자동 크롤러로는 부족할 수 있습니다. 별도의 설정 파일을 사용하는 더 견고한 접근 방식을 소개합니다.
// 동적 라우트 목록을 생성하는 유틸리티
import { prisma } from './prisma'
export async function getAllStaticRoutes(): Promise<string[]> {
const routes: string[] = []
// 블로그 기사
const articles = await prisma.article.findMany({
where: { published: true },
select: { slug: true, category: { select: { slug: true } } }
})
for (const article of articles) {
routes.push(`/blog/${article.category.slug}/${article.slug}`)
}
// 상품 페이지
const products = await prisma.product.findMany({
where: { active: true },
select: { slug: true }
})
for (const product of products) {
routes.push(`/products/${product.slug}`)
}
// 태그 페이지
const tags = await prisma.tag.findMany({
select: { slug: true }
})
for (const tag of tags) {
routes.push(`/tags/${tag.slug}`)
}
return routes
}Vue.js / Nuxt.js 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
routeRules로 하이브리드 렌더링하기
하이브리드 렌더링은 Nuxt 3의 대표 기능입니다. 라우트마다 서로 다른 렌더링 규칙을 정의해 SSR과 SSG의 장점을 결합할 수 있게 해줍니다.
// 하이브리드 렌더링의 고급 설정
export default defineNuxtConfig({
routeRules: {
// 마케팅 페이지: 사전 렌더링하고 장기간 캐시
'/': { prerender: true },
'/pricing': { prerender: true },
'/features/**': { prerender: true },
// 블로그: ISR (Incremental Static Regeneration)
// 매시간 재검증
'/blog/**': {
isr: 3600,
prerender: true
},
// 문서: 재검증을 포함한 CDN 캐시
'/docs/**': {
swr: 86400, // Stale-while-revalidate
prerender: true
},
// 이커머스: 짧은 캐시를 가진 SSR
'/products/**': {
ssr: true,
cache: {
maxAge: 60,
staleMaxAge: 300
}
},
// 장바구니와 결제: 클라이언트 사이드 전용
'/cart': { ssr: false },
'/checkout/**': { ssr: false },
// 대시보드: SPA 모드
'/dashboard/**': {
ssr: false,
// 사전 렌더링 비활성화
prerender: false
},
// API 라우트: 기본적으로 캐시 없음
'/api/**': {
cors: true,
headers: {
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE'
}
}
}
})이 설정은 일반적인 현대 애플리케이션 아키텍처를 보여줍니다. 공개 페이지는 SSG로 SEO에 최적화되고, 인터랙티브한 영역은 클라이언트 사이드 렌더링을 사용합니다.
데이터 캐싱을 통한 성능 최적화
페이지 캐싱뿐 아니라 Nuxt 3은 가져온 데이터를 캐싱할 수 있게 해줍니다. 이 전략은 API 부하를 줄이고 응답 시간을 개선합니다.
// 데이터 캐싱이 적용된 API 엔드포인트
import { getArticleBySlug } from '~/server/utils/articles'
export default defineCachedEventHandler(
async (event) => {
const slug = getRouterParam(event, 'slug')
if (!slug) {
throw createError({
statusCode: 400,
message: 'slug가 없습니다'
})
}
const article = await getArticleBySlug(slug)
if (!article) {
throw createError({
statusCode: 404,
message: '기사를 찾을 수 없습니다'
})
}
return article
},
{
// slug 기반 캐시 키
getKey: (event) => `article-${getRouterParam(event, 'slug')}`,
// 캐시 유지 시간: 1시간
maxAge: 3600,
// Stale-while-revalidate: 업데이트 중에도 오래된 캐시 제공
staleMaxAge: 7200,
// 태그 기반 무효화
tags: ['articles']
}
)콘텐츠가 변경될 때 캐시를 무효화하기 위해 Nuxt는 태그 시스템을 제공합니다.
// 캐시 무효화를 동반한 기사 업데이트
import { updateArticle } from '~/server/utils/articles'
export default defineEventHandler(async (event) => {
const slug = getRouterParam(event, 'slug')
const body = await readBody(event)
// 기사 업데이트
const article = await updateArticle(slug, body)
// 이 기사에 대한 캐시 무효화
await useStorage('cache').removeItem(`nitro:handlers:article-${slug}`)
// 또는 태그 기반 무효화 (모든 기사)
// await useStorage('cache').clear('articles')
return article
})다중 인스턴스 환경의 프로덕션에서는 인메모리 캐시만으로는 충분하지 않습니다. 인스턴스 간 일관성을 보장하기 위해 Nitro 설정을 통해 Redis 또는 다른 분산 시스템을 구성하는 것을 권장합니다.
SEO와 메타데이터 관리
SSR은 메타데이터를 서버 측에서 생성함으로써 SEO를 최적화할 수 있게 해줍니다. Nuxt 3은 메타 태그를 동적으로 관리하기 위한 여러 가지 접근 방식을 제공합니다.
<script setup lang="ts">
// pages/blog/[slug].vue
// SEO가 최적화된 블로그 페이지
const route = useRoute()
const { data: article } = await useFetch(`/api/articles/${route.params.slug}`)
// 기사에 기반한 동적 SEO 설정
useSeoMeta({
title: article.value?.title,
description: article.value?.excerpt,
ogTitle: article.value?.title,
ogDescription: article.value?.excerpt,
ogImage: article.value?.coverImage,
ogType: 'article',
twitterCard: 'summary_large_image',
twitterTitle: article.value?.title,
twitterDescription: article.value?.excerpt,
twitterImage: article.value?.coverImage
})
// Google을 위한 구조화된 데이터
useHead({
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Article',
headline: article.value?.title,
description: article.value?.excerpt,
image: article.value?.coverImage,
datePublished: article.value?.publishedAt,
dateModified: article.value?.updatedAt,
author: {
'@type': 'Organization',
name: 'SharpSkill'
}
})
}
]
})
</script>정적 페이지의 경우 메타데이터를 컴포넌트에서 직접 정의할 수 있습니다.
<script setup lang="ts">
// pages/about.vue
// SEO가 적용된 정적 페이지
definePageMeta({
title: '소개'
})
useSeoMeta({
title: 'SharpSkill 소개 | 기술 면접 준비',
description: '기술 면접 준비 플랫폼 SharpSkill을 소개합니다. 사명은 개발자가 기술 면접에서 성공할 수 있도록 돕는 것입니다.',
ogTitle: 'SharpSkill 소개',
ogDescription: '기술 면접 준비 플랫폼',
ogImage: '/images/og-about.webp'
})
</script>배포와 프로덕션 고려 사항
배포 선택은 사용하는 렌더링 모드에 따라 달라집니다. 주요 옵션과 그 설정은 다음과 같습니다.
// 다양한 배포 환경을 위한 설정
export default defineNuxtConfig({
nitro: {
// 대상 플랫폼에 맞춘 프리셋
// preset: 'vercel', // Vercel
// preset: 'netlify', // Netlify
// preset: 'cloudflare-pages', // Cloudflare
// preset: 'node-server', // 일반적인 Node.js
// 프로덕션 환경의 Node.js를 위한 설정
preset: 'node-server',
// 응답 압축
compressPublicAssets: true,
// 캐시 저장소 설정
storage: {
cache: {
driver: 'redis',
url: process.env.REDIS_URL
}
}
},
// 런타임 환경 변수
runtimeConfig: {
// 비밀 값(클라이언트에 노출되지 않음)
apiSecret: process.env.API_SECRET,
// 공개 설정
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api'
}
}
})정적 배포의 경우 npm run generate 명령은 어떤 정적 파일 호스트에도 배포할 준비가 된 .output/public 폴더를 생성합니다.
# 정적 생성
npm run generate
# .output/public의 내용은 다음 위치에 배포할 수 있습니다:
# - Vercel(자동 감지)
# - Netlify(자동 설정)
# - GitHub Pages
# - S3 + CloudFront
# - 임의의 CDN 또는 정적 파일 서버연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
결론
Nuxt 3은 Vue.js 애플리케이션을 렌더링하기 위한 탁월한 유연성을 제공합니다. SSR, SSG, 하이브리드 렌더링 중 무엇을 선택할지는 각 프로젝트의 구체적인 요구 사항에 달려 있습니다.
주요 포인트:
✅ SSR: 좋은 SEO가 필요한 동적 콘텐츠에 이상적(이커머스, 뉴스 사이트)
✅ SSG: 안정적인 콘텐츠에 적합(블로그, 문서, 마케팅 사이트)
✅ 하이브리드: 다양한 요구를 가진 복잡한 애플리케이션에 가장 적합한 접근 방식
✅ useFetch/useAsyncData: 자동 하이드레이션과 캐시 관리
✅ routeRules: 각 라우트의 동작에 대한 세밀한 설정
✅ 캐싱: 프로덕션 성능을 최적화하기 위한 다양한 전략
하이브리드 렌더링과 잘 설계된 캐싱 전략을 결합하면 SEO에 최적화된 고성능 애플리케이션을 구축하면서도 Single Page Applications의 인터랙티브함을 유지할 수 있습니다.
태그
공유
관련 기사

Vue.js 면접 핵심 질문: 합격을 위한 25문항
Vue.js 면접을 위한 25개의 핵심 질문. 반응성부터 composables까지, 다음 면접에서 빛날 핵심 개념을 정리합니다.

Vue 3 Pinia vs Vuex 완벽 비교: 2026년 상태 관리 전략과 면접 핵심 질문
Vue 3 생태계에서 Pinia와 Vuex를 비교 분석합니다. Options Store와 Setup Store 패턴, TypeScript 통합, 크로스 스토어 구성, SSR 지원, Vuex에서 Pinia로의 마이그레이션 전략, 그리고 2026년 면접에서 자주 출제되는 상태 관리 질문을 코드 예제와 함께 정리합니다.

Vue 3 Composition API 완벽 가이드: 리액티비티 마스터하기
Vue 3 Composition API 실전 가이드입니다. ref, reactive, computed, watch, 컴포저블을 배워 고성능 Vue 애플리케이션을 구축하십시오.