본문으로 건너뛰기
Advertisement

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 프로젝트를 실제로 설정하고 디렉토리 구조를 자세히 알아보겠습니다.

Advertisement