15.1 Nuxt 소개 — Vue 기반 풀스택 프레임워크
Nuxt란 무엇인가?
Nuxt는 Vue.js를 기반으로 한 풀스택 웹 프레임워크입니다. Vue만으로는 클라이언트 사이드 렌더링(CSR)만 가능하지만, Nuxt를 사용하면 서버 사이드 렌더링(SSR), 정적 사이트 생성(SSG), 그리고 풀스택 API 서버까지 하나의 프로젝트에서 구현할 수 있습니다.
Nuxt 3는 2022년 말 정식 출시된 버전으로, Vue 3, Vite, TypeScript를 기본으로 채택하여 현대적인 웹 개발 경험을 제공합니다.
Nuxt가 해결하는 문제들
| 문제 | Vue만 사용 | Nuxt 사용 |
|---|---|---|
| SEO 최적화 | 어려움 (CSR) | 기본 제공 (SSR/SSG) |
| 라우팅 설정 | 수동 설정 필요 | 파일 기반 자동 생성 |
| API 서버 | 별도 백엔드 필요 | 내장 API 라우트 |
| 코드 분할 | 수동 설정 | 자동 처리 |
| SEO 메타 태그 | 수동 관리 | useHead 컴포저블 |
| 상태 관리 | Pinia 별도 설치 | 내장 useState |
렌더링 모드 이해하기
Nuxt의 가장 강력한 기능 중 하나는 렌더링 모드를 유연하게 선택할 수 있다는 점입니다.
1. SSR (Server-Side Rendering)
서버에서 HTML을 완성한 뒤 브라우저로 전송합니다. SEO에 유리하고 초기 로딩이 빠릅니다.
요청 → 서버에서 Vue 렌더링 → 완성된 HTML 전송 → 브라우저에서 하이드레이션
2. SSG (Static Site Generation)
빌드 시점에 모든 페이지를 HTML로 생성합니다. 블로그, 문서 사이트에 적합합니다.
빌드 → 모든 페이지 HTML 생성 → CDN 배포 → 요청 시 정적 파일 반환
3. CSR (Client-Side Rendering)
기존 Vue SPA 방식입니다. 브라우저에서 JavaScript로 렌더링합니다.
요청 → 빈 HTML + JS 번들 전송 → 브라우저에서 렌더링
4. ISR (Incremental Static Regeneration)
SSG와 SSR의 장점을 결합합니다. 정적 페이지를 주기적으로 재생성합니다.
최초 요청 → 서버에서 생성 후 캐시 → 이후 요청은 캐시된 페이지 반환 → 주기적 갱신
파일 기반 라우팅
Nuxt의 핵심 기능 중 하나는 파일 시스템 기반 라우팅입니다. pages/ 디렉토리에 파일을 생성하면 자동으로 라우트가 만들어집니다.
기본 라우팅
pages/
├── index.vue → /
├── about.vue → /about
├── contact.vue → /contact
└── blog/
├── index.vue → /blog
└── [slug].vue → /blog/:slug (동적 라우트)
동적 라우트 예제
<!-- pages/blog/[slug].vue -->
<template>
<div>
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
</div>
</template>
<script setup lang="ts">
// useRoute()로 현재 라우트 파라미터 접근
const route = useRoute()
const slug = route.params.slug // /blog/hello-world → slug = 'hello-world'
const { data: post } = await useFetch(`/api/posts/${slug}`)
</script>
중첩 라우트
pages/
└── user/
├── [id]/
│ ├── index.vue → /user/:id
│ └── settings.vue → /user/:id/settings
└── index.vue → /user
캐치올 라우트
pages/
└── [...slug].vue → /a, /a/b, /a/b/c 모두 매칭
<!-- pages/[...slug].vue -->
<script setup lang="ts">
const route = useRoute()
// /docs/guide/intro → slug = ['docs', 'guide', 'intro']
console.log(route.params.slug)
</script>
Auto-import 시스템
Nuxt 3의 혁신적인 기능 중 하나는 **자동 임포트(Auto-import)**입니다. Vue 컴포저블, 유틸리티, 컴포넌트를 import 문 없이 바로 사용할 수 있습니다.
자동 임포트 대상
Vue 컴포저블
<script setup lang="ts">
// import { ref, computed, watch } from 'vue' 불필요!
const count = ref(0)
const doubled = computed(() => count.value * 2)
watch(count, (newVal) => {
console.log('count changed:', newVal)
})
</script>
Nuxt 컴포저블
<script setup lang="ts">
// import 없이 바로 사용
const route = useRoute()
const router = useRouter()
const config = useRuntimeConfig()
const { data } = await useFetch('/api/users')
</script>
composables/ 디렉토리
// composables/useCounter.ts
export const useCounter = () => {
const count = ref(0)
const increment = () => count.value++
const decrement = () => count.value--
return { count, increment, decrement }
}
<!-- 어디서든 import 없이 사용 -->
<script setup lang="ts">
const { count, increment } = useCounter()
</script>
utils/ 디렉토리
// utils/format.ts
export const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('ko-KR').format(date)
}
export const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('ko-KR', {
style: 'currency',
currency: 'KRW'
}).format(amount)
}
<script setup lang="ts">
// import 없이 바로 사용
const price = formatCurrency(15000) // '₩15,000'
</script>
컴포넌트 자동 임포트
components/
├── Button.vue → <Button />
├── ui/
│ ├── Card.vue → <UiCard />
│ └── Modal.vue → <UiModal />
└── base/
└── Input.vue → <BaseInput />
실전 예제: 간단한 블로그 앱
아래는 Nuxt의 핵심 기능을 활용한 간단한 블로그 앱 예제입니다.
프로젝트 구조
my-blog/
├── pages/
│ ├── index.vue # 블로그 목록
│ └── posts/
│ └── [id].vue # 포스트 상세
├── components/
│ └── PostCard.vue # 포스트 카드 컴포넌트
├── composables/
│ └── usePosts.ts # 포스트 관련 로직
├── server/
│ └── api/
│ ├── posts/
│ │ ├── index.get.ts # GET /api/posts
│ │ └── [id].get.ts # GET /api/posts/:id
└── nuxt.config.ts
서버 API 작성
// server/api/posts/index.get.ts
export default defineEventHandler(async (event) => {
// 실제로는 DB에서 가져오지만, 예제에서는 하드코딩
return [
{
id: 1,
title: 'Nuxt 3 시작하기',
summary: 'Nuxt 3의 핵심 기능을 알아봅니다.',
date: '2024-01-15',
author: '김철수'
},
{
id: 2,
title: 'Vue 3 Composition API',
summary: 'Composition API의 장점과 사용법을 설명합니다.',
date: '2024-01-20',
author: '이영희'
},
{
id: 3,
title: 'TypeScript와 Nuxt',
summary: 'Nuxt에서 TypeScript를 효과적으로 사용하는 방법.',
date: '2024-01-25',
author: '박민수'
}
]
})
// server/api/posts/[id].get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
const posts: Record<string, object> = {
'1': {
id: 1,
title: 'Nuxt 3 시작하기',
content: `
# Nuxt 3 시작하기
Nuxt 3는 Vue.js 기반의 풀스택 프레임워크입니다...
## 설치
\`\`\`bash
npx nuxi@latest init my-app
\`\`\`
`,
date: '2024-01-15',
author: '김철수'
}
}
const post = posts[id as string]
if (!post) {
throw createError({ statusCode: 404, message: '포스트를 찾을 수 없습니다.' })
}
return post
})
컴포저블 작성
// composables/usePosts.ts
interface Post {
id: number
title: string
summary: string
date: string
author: string
}
export const usePosts = () => {
const posts = useState<Post[]>('posts', () => [])
const fetchPosts = async () => {
const { data } = await useFetch<Post[]>('/api/posts')
if (data.value) {
posts.value = data.value
}
}
return { posts, fetchPosts }
}
블로그 목록 페이지
<!-- pages/index.vue -->
<template>
<div class="container">
<h1>블로그</h1>
<div v-if="pending" class="loading">
로딩 중...
</div>
<div v-else-if="error" class="error">
오류가 발생했습니다: {{ error.message }}
</div>
<div v-else class="post-list">
<PostCard
v-for="post in posts"
:key="post.id"
:post="post"
/>
</div>
</div>
</template>
<script setup lang="ts">
interface Post {
id: number
title: string
summary: string
date: string
author: string
}
// useFetch는 SSR에서도 동작하므로 SEO에 유리
const { data: posts, pending, error } = await useFetch<Post[]>('/api/posts')
// SEO 설정
useHead({
title: '블로그 - 최신 글 목록',
meta: [
{ name: 'description', content: 'Vue와 Nuxt에 대한 최신 글을 만나보세요.' }
]
})
</script>
<style scoped>
.container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
.post-list {
display: grid;
gap: 1.5rem;
}
.loading, .error {
text-align: center;
padding: 2rem;
}
</style>
포스트 카드 컴포넌트
<!-- components/PostCard.vue -->
<template>
<article class="post-card">
<NuxtLink :to="`/posts/${post.id}`">
<h2>{{ post.title }}</h2>
</NuxtLink>
<p>{{ post.summary }}</p>
<footer>
<span>{{ post.author }}</span>
<time>{{ formatDate(post.date) }}</time>
</footer>
</article>
</template>
<script setup lang="ts">
interface Post {
id: number
title: string
summary: string
date: string
author: string
}
defineProps<{
post: Post
}>()
// utils/format.ts의 formatDate 자동 임포트
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('ko-KR', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
}
</script>
포스트 상세 페이지
<!-- pages/posts/[id].vue -->
<template>
<div class="post-detail">
<NuxtLink to="/">← 목록으로</NuxtLink>
<article v-if="post">
<h1>{{ post.title }}</h1>
<div class="meta">
<span>{{ post.author }}</span>
<time>{{ post.date }}</time>
</div>
<!-- 실제로는 마크다운 렌더러 사용 -->
<div class="content" v-html="post.content" />
</article>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
interface Post {
id: number
title: string
content: string
date: string
author: string
}
const { data: post, error } = await useFetch<Post>(`/api/posts/${route.params.id}`)
if (error.value) {
throw createError({ statusCode: 404, message: '포스트를 찾을 수 없습니다.' })
}
// 동적 SEO 메타 태그
useHead({
title: () => post.value ? `${post.value.title} - 블로그` : '블로그',
meta: [
{
name: 'description',
content: () => post.value?.content?.slice(0, 160) || ''
}
]
})
</script>
고수 팁
1. Nitro 서버 엔진 이해하기
Nuxt 3는 Nitro라는 서버 엔진을 사용합니다. Nitro는 다양한 배포 환경을 지원합니다.
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: 'cloudflare-pages', // Cloudflare Pages용
// preset: 'vercel', // Vercel용
// preset: 'node-server', // Node.js 서버용
// preset: 'static', // 정적 배포용
}
})
2. 플러그인으로 전역 기능 추가
// plugins/errorHandler.ts
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.config.errorHandler = (error, instance, info) => {
console.error('Vue 에러:', error, info)
// 에러 리포팅 서비스로 전송
}
nuxtApp.hook('vue:error', (error) => {
console.error('Nuxt 에러 훅:', error)
})
})
3. 미들웨어로 인증 처리
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const user = useSupabaseUser() // 또는 자체 인증 상태
// 인증이 필요한 페이지에 비로그인 접근 시 리다이렉트
if (!user.value && to.path !== '/login') {
return navigateTo('/login')
}
})
<!-- pages/dashboard.vue -->
<script setup lang="ts">
// 이 페이지에만 미들웨어 적용
definePageMeta({
middleware: 'auth'
})
</script>
4. 레이아웃 시스템 활용
<!-- layouts/default.vue -->
<template>
<div>
<AppHeader />
<main>
<slot /> <!-- 페이지 내용이 여기에 들어감 -->
</main>
<AppFooter />
</div>
</template>
<!-- layouts/admin.vue -->
<template>
<div class="admin-layout">
<AdminSidebar />
<main>
<slot />
</main>
</div>
</template>
<!-- pages/admin/dashboard.vue -->
<script setup lang="ts">
definePageMeta({
layout: 'admin' // admin 레이아웃 사용
})
</script>
5. 타입 안전한 API 라우트
// server/api/users/[id].get.ts
interface User {
id: number
name: string
email: string
}
export default defineEventHandler(async (event): Promise<User> => {
const id = parseInt(getRouterParam(event, 'id') as string)
// 실제로는 DB 쿼리
return {
id,
name: '김철수',
email: 'kim@example.com'
}
})
<!-- pages/users/[id].vue -->
<script setup lang="ts">
const route = useRoute()
// 타입 추론이 자동으로 됩니다
const { data: user } = await useFetch(`/api/users/${route.params.id}`)
// user.value는 { id: number, name: string, email: string } 타입
</script>
6. 서버-클라이언트 코드 분리
// server/utils/db.ts (서버에서만 실행)
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_KEY! // 서버에서만 사용하는 비밀 키
)
// composables/useAuth.ts (클라이언트에서 실행)
export const useAuth = () => {
const user = useState('user', () => null)
const login = async (email: string, password: string) => {
// 공개 키만 사용
const response = await $fetch('/api/auth/login', {
method: 'POST',
body: { email, password }
})
user.value = response.user
}
return { user, login }
}
정리
Nuxt 3는 단순히 Vue에 SSR을 추가한 프레임워크가 아닙니다. 파일 기반 라우팅, Auto-import, 내장 서버 API, 유연한 렌더링 모드를 통해 풀스택 웹 애플리케이션을 빠르게 개발할 수 있는 종합 솔루션입니다.
특히 Auto-import 시스템은 개발 경험을 크게 향상시킵니다. 매번 import 문을 작성할 필요 없이 바로 사용할 수 있어 코드가 간결해지고 생산성이 높아집니다.
다음 챕터에서는 Nuxt 프로젝트를 실제로 설정하고 디렉토리 구조를 자세히 알아보겠습니다.