Nuxt는 Vue.js 기반의 SSR(Server-Side Rendering) 프레임워크이며, Vue 3을 기반으로 동작한다.
기본적으로 유니버설 렌더링을 채택해 초기 렌더링 성능과 SEO를 모두 고려한 구조를 제공한다.
Composition API vs Options API
Options API (Vue 2 중심)
data,methods,computed,mounted등 옵션 단위로 역할을 분리하여 코드 작성this컨텍스트에 의존- TypeScript 사용 시
this확장으로 인해 타입 관리가 불편함 - Vue 2는 2023년 12월 31일부로 지원 종료(EOL)됨
- Nuxt 3에서도 사용 가능하지만, 권장되지 않음
<!-- Vue 2 - Options API 예시 -->
<template>
<div>
<h1></h1>
<p>카운트: 8</p>
<button @click="increment">증가</button>
<button @click="setMessage('Vue 2로 변경됨')">메시지 변경</button>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
data() {
return {
count: 0 as number,
message: '안녕하세요, Vue 2!' as string
}
},
methods: {
increment(this: Vue & { count: number }) {
this.count++
},
setMessage(this: Vue & { message: string }, msg: string) {
this.message = msg
}
}
})
</script>
Composition API (Vue 3 & Nuxt 기본)
<script setup>블록 내에서 함수 기반으로 로직 구성- ref, computed, onMounted 등을 직접 사용
- this를 사용하지 않아 타입 안정성 우수
- useXXX 형태의 Composable을 통해 로직 재사용 가능
- Nuxt는 Composition API 사용을 전제로 설계됨
<!-- Vue 3 - Composition API 예시 -->
<template>
<div>
<h1></h1>
<p>카운트: 8</p>
<button @click="increment">증가</button>
<button @click="setMessage('Vue 3로 변경됨')">메시지 변경</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const count = ref<number>(0)
const message = ref<string>('안녕하세요, Vue 3!')
function increment() {
count.value++
}
function setMessage(msg: string) {
message.value = msg
}
</script>
주의 사항
<script setup\>최상위 코드는 컴포넌트 인스턴스 생성 전에 실행- SSR 환경에서는 window, document 같은 Browser API 사용 금지
- 반드시 onMounted() 내부에서 사용해야 함
유니버설 렌더링 (Universal Rendering)
📌 Nuxt의 기본 렌더링 방식으로, SSR + CSR의 장점을 결합한 하이브리드 렌더링 전략
동작 흐름
- 사용자가 페이지 요청
- 서버에서 Vue 코드를 실행해 완성된 HTML 생성 (SSR)
- 브라우저는 HTML을 즉시 화면에 표시
- 이후 JavaScript 번들을 다운로드 및 실행
- 서버 HTML과 Vue 상태를 연결 → 하이드레이션(Hydration)
- 이후 페이지 이동은 CSR 방식(SPA처럼 동작)
Hydration이란?
- 서버에서 생성된 HTML을 다시 렌더링하지 않고
- 이벤트 리스너와 반응형 상태만 주입하는 과정
Code Splitting (코드 분할)
- Nuxt는 페이지 / 컴포넌트 단위로 자동 코드 분할
- 초기 로딩 시 필요한 JS 청크만 로드
- 다른 페이지 이동 시 필요한 청크만 추가 다운로드
Prefetching
-
에 마우스를 올리거나 - 링크가 뷰포트에 들어오면
- 필요한 리소스를 백그라운드에서 미리 로드
<NuxtLink to="/hello">Hello page</NuxtLink>
주의할 점
Browser API 사용
- SSR 환경에서는 실행 불가
- 반드시 onMounted() 내부에서 사용
onMounted(() => { window.addEventListener('resize', ...) })
HTTP 통신 중복 호출 이슈
$fetch
- Nuxt에서 제공하는 전역 HTTP 클라이언트
- 실행 환경에 따라 자동 선택
- 서버: node-fetch-native
- 브라우저: native fetch
- Axios보다 가볍고 SSR/CSR 환경에 최적화
문제점 (SSR 컴포넌트에서 $fetch 사용 시)
- 서버 렌더링 시 API 호출
- 클라이언트 하이드레이션 시 동일 API 재호출
- 데이터 불일치 시 Hydration Mismatch 오류 발생
해결 방법: useFetch
- Nuxt에서 제공하는 SSR 전용 데이터 패칭 컴포저블
- 서버에서 가져온 데이터를 클라이언트로 자동 전달
- 중복 호출 방지 + 상태 일관성 보장
const { data, pending, error } = await useFetch('/api/data')
사용 기준
- 초기 데이터 패칭 → useFetch
- 사용자 이벤트 / onMounted 이후 → $fetch
useHead
- 페이지별 head 메타 정보를 동적으로 설정하는 Nuxt 컴포저블
- 부모/자식 컴포넌트에서 동시에 사용 시 → 자식 컴포넌트 설정이 우선
const { data, status } = await useEventFetch<IResponse<IOgData>>(
`/api/og/${params.code}`,
)
const ogData = data.value
if (ogData?.status === 200 && status.value === 'success') {
const { title, description, imageUrl, url } = ogData.data
useHead({
title,
meta: [
{ property: 'og:type', content: 'website' },
{ property: 'og:title', content: title },
{ property: 'og:description', content: description },
{ property: 'og:image', content: image },
{ property: 'og:url', content: url },
{ name: 'description', content: description },
{ name: 'image', content: imageUrl },
// Facebook
{ property: 'fb:app_id', content: 'app_id' },
{ property: 'fb:image:type', content: `image/jpg` },
// Twitter
{ name: 'twitter:title', content: title },
{ name: 'twitter:description', content: description },
{ name: 'twitter:image', content: imageUrl },
],
})
}
Auto-import
Nuxt는 특정 디렉토리 및 API에 대해 자동 import 기능을 제공한다.
자동으로 import 되는 대상
- Vue API (ref, computed, watch, onMounted 등)
- Nuxt Composable (useFetch, useRoute, useRouter, useHead 등)
- /composables, /utils, /plugins 디렉토리 내부 파일 👉 import 문 없이 바로 사용 가능하여 코드 가독성과 생산성 향상
Nuxt 3 핵심 디렉토리 구조
Nuxt 3는 규칙 기반의 폴더 구조를 사용하여 개발 생산성을 높인다.
- pages/: 파일 시스템 기반 라우팅을 처리합니다. (
app.vue가 있으면 선택 사항) - components/: 컴포넌트를 정의하며, 자동으로 import 됩니다.
- layouts/: 페이지의 공통 레이아웃을 정의합니다. (
<NuxtLayout>) - server/: API 엔드포인트 및 서버 미들웨어를 작성합니다. (Nitro 엔진)
- composables/:
useFoo()형태의 훅을 작성하면 자동 import 됩니다. - middleware/: 페이지 이동 전 실행되는 네비게이션 가드를 정의합니다.
- plugins/: Vue 인스턴스 생성 시점에 실행될 플러그인을 정의합니다.
상태 관리 (State Management): useState
Nuxt 3에서는 useState라는 SSR 친화적인 상태 관리 훅을 제공
일반적인 ref를 컴포넌트 밖에서 사용하면, 서버와 클라이언트 간 상태 불일치(Hydration Mismatch)가 발생하거나, 요청 간에 상태가 공유되는(Cross-Request Pollution) 보안 문제가 생길 수 있습니다.
// composables/useCounter.ts
export const useCounter = () => {
return useState('counter', () => 0)
}
<script setup>
const count = useCounter()
</script>
- SSR 지원: 서버에서 초기화된 상태를 클라이언트로 직렬화하여 전달합니다.
- 범위 보장: 각 요청(Request)마다 독립적인 상태를 유지합니다.
서버 엔진 Nitro
Nuxt 3의 강력한 서버 엔진인 Nitro는 다음과 같은 특징이 있습니다.
- 크로스 플랫폼 배포: Node.js, Deno, Serverless(Vercel, AWS Lambda), Edge Worker 등 다양한 환경에 배포 가능 (설정 한 줄로 변환)
- 서버 API 라우트:
server/api/디렉토리에 파일을 만들면 자동으로 API 엔드포인트가 생성됩니다.
// server/api/hello.ts
export default defineEventHandler((event) => {
return {
message: 'Hello World'
}
})
→ GET /api/hello 로 호출 가능
하이브리드 렌더링 (Hybrid Rendering)
Nuxt 3는 페이지별로 렌더링 전략을 다르게 가져갈 수 있는 Route Rules 기능을 제공합니다.
nuxt.config.ts에서 설정할 수 있습니다.
export default defineNuxtConfig({
routeRules: {
// 정적 페이지 (빌드 시점에 생성)
'/about': { prerender: true },
// SWR (Stale-While-Revalidate): 1시간마다 캐시 갱신
'/blog/**': { swr: 3600 },
// API 응답 캐싱
'/api/**': { cache: { maxAge: 60 } },
// SSR 비활성화 (SPA 모드)
'/admin/**': { ssr: false }
}
})
이를 통해 정적 사이트(SSG + JAMStack)와 동적 서버 사이드 렌더링(SSR)의 장점을 동시에 취할 수 있습니다.