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의 장점을 결합한 하이브리드 렌더링 전략

동작 흐름

  1. 사용자가 페이지 요청
  2. 서버에서 Vue 코드를 실행해 완성된 HTML 생성 (SSR)
  3. 브라우저는 HTML을 즉시 화면에 표시
  4. 이후 JavaScript 번들을 다운로드 및 실행
  5. 서버 HTML과 Vue 상태를 연결 → 하이드레이션(Hydration)
  6. 이후 페이지 이동은 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 사용 시)

  1. 서버 렌더링 시 API 호출
  2. 클라이언트 하이드레이션 시 동일 API 재호출
  3. 데이터 불일치 시 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)의 장점을 동시에 취할 수 있습니다.

chanhee.kim's profile image

chanhee.kim

2026-01-31 20:15

Read more posts by this author