본문으로 건너뛰기 Nuxt 3 완벽 가이드 | Vue·SSR·Composables·Nitro·Server Routes

Nuxt 3 완벽 가이드 | Vue·SSR·Composables·Nitro·Server Routes

Nuxt 3 완벽 가이드 | Vue·SSR·Composables·Nitro·Server Routes

이 글의 핵심

Nuxt 3 완벽 가이드에 대해 정리한 개발 블로그 글입니다. Nuxt 3로 풀스택 Vue 앱을 구축하는 완벽 가이드입니다. Auto-imports, Composables, Nitro 엔진, Server Routes, 배포까지 실전 예제로 정리했고, SSR·SSG·CSR 파이프라인,… 개념과 예제 코드를 단계적으로 다루며, 실무·학습에 참고할 수 있도록 구성했습니다. 관련 키워드:…

이 글의 핵심

Nuxt 3로 풀스택 Vue 앱을 구축하는 완벽 가이드입니다. Auto-imports, Composables, Nitro 엔진, Server Routes, 배포까지 실전 예제로 정리했으며, SSR·SSG·CSR 파이프라인, unimport·#imports, Nitro 프리셋·.output, 모듈·훅, 프로덕션 운영 패턴까지 내부 동작을 심화해 다룹니다.

실무 경험 공유: Vue SPA를 Nuxt 3로 전환하면서, SEO를 크게 개선하고 초기 로딩 속도를 3배 향상시킨 경험을 공유합니다.

들어가며: “Vue SPA는 SEO가 약해요”

실무에서 마주치는 문제들

검색 엔진에 안 잡혀요

SPA는 SEO가 약합니다. Nuxt SSR로 해결합니다. 초기 로딩이 느려요

모든 JavaScript를 다운로드해야 합니다. Nuxt는 서버에서 렌더링합니다. import가 번거로워요

매번 import 해야 합니다. Nuxt는 자동 import합니다.

1. Nuxt 3란?

핵심 특징

Nuxt 3는 Vue 3 기반 풀스택 프레임워크입니다. 주요 장점

  • Auto-imports: 자동 import
  • Nitro: 빠른 서버 엔진
  • SSR/SSG: 렌더링 모드 선택
  • Server Routes: API 내장
  • TypeScript: 완벽한 지원

2. 프로젝트 생성

npx nuxi@latest init my-app
cd my-app
npm install
npm run dev

3. 파일 기반 라우팅

페이지

<!-- pages/index.vue -->
<template>
  <div>
    <h1>Home</h1>
    <NuxtLink to="/about">About</NuxtLink>
  </div>
</template>
<!-- pages/about.vue -->
// 실행 예제
<template>
  <div>
    <h1>About</h1>
  </div>
</template>

동적 라우트

<!-- pages/blog/[slug].vue -->
<script setup lang="ts">
const route = useRoute();
const slug = route.params.slug;
const { data: post } = await useFetch(`/api/posts/${slug}`);
</script>
<template>
  <article>
    <h1>{{ post.title }}</h1>
    <p>{{ post.content }}</p>
  </article>
</template>

4. Composables

useFetch

<script setup lang="ts">
const { data, pending, error, refresh } = await useFetch('/api/users');
</script>
<template>
  <div>
    <div v-if="pending">Loading...</div>
    <div v-else-if="error">Error: {{ error.message }}</div>
    <ul v-else>
      <li v-for="user in data" :key="user.id">
        {{ user.name }}
      </li>
    </ul>
    <button @click="refresh">Refresh</button>
  </div>
</template>

useAsyncData

<script setup lang="ts">
const { data: posts } = await useAsyncData('posts', () => 
  $fetch('/api/posts')
);
</script>

커스텀 Composable

// composables/useAuth.ts
export const useAuth = () => {
  const user = useState('user', () => null);
  const login = async (email: string, password: string) => {
    const response = await $fetch('/api/login', {
      method: 'POST',
      body: { email, password },
    });
    user.value = response.user;
  };
  const logout = async () => {
    await $fetch('/api/logout', { method: 'POST' });
    user.value = null;
  };
  return { user, login, logout };
};

5. Server Routes

API 엔드포인트

// server/api/users.get.ts
export default defineEventHandler(async (event) => {
  const users = await prisma.user.findMany();
  return users;
});
// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = parseInt(event.context.params.id);
  const user = await prisma.user.findUnique({ where: { id } });
  if (!user) {
    throw createError({
      statusCode: 404,
      statusMessage: 'User not found',
    });
  }
  return user;
});
// server/api/users.post.ts
export default defineEventHandler(async (event) => {
  const body = await readBody(event);
  const user = await prisma.user.create({
    data: {
      name: body.name,
      email: body.email,
    },
  });
  return user;
});

6. Middleware

인증 Middleware

// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const { user } = useAuth();
  if (!user.value && to.path !== '/login') {
    return navigateTo('/login');
  }
});

사용

<!-- pages/dashboard.vue -->
<script setup lang="ts">
definePageMeta({
  middleware: 'auth',
});
</script>
<template>
  <div>
    <h1>Dashboard</h1>
  </div>
</template>

7. Layouts

아래는 기본 레이아웃 예제 코드입니다.

<!-- layouts/default.vue -->
<template>
  <div>
    <header>
      <nav>
        <NuxtLink to="/">Home</NuxtLink>
        <NuxtLink to="/about">About</NuxtLink>
      </nav>
    </header>
    <main>
      <slot />
    </main>
    <footer>
      <p>&copy; 2026 My App</p>
    </footer>
  </div>
</template>
<!-- pages/index.vue -->
<script setup lang="ts">
definePageMeta({
  layout: 'default',
});
</script>

8. 배포

Static (SSG)

npm run generate

Server (SSR)

npm run build
node .output/server/index.mjs

Docker

FROM node:20-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/.output .output
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]

9. 렌더링 모드(SSR·SSG·CSR) 내부 동작

Nuxt 3는 한 코드베이스에서 클라이언트·서버 번들을 동시에 구성합니다. nuxt build 시 Vite가 클라이언트 엔트리서버 엔트리(Vue SSR용)를 각각 트리셰이킹하여 산출하며, 런타임에서는 요청 경로에 따라 HTML을 서버에서 조립할지, 정적 HTML만 내보낼지, 브라우저에서만 Vue를 기동할지가 갈립니다.

SSR(서버 사이드 렌더링)

요청이 들어오면 Nitro가 Node(또는 프리셋 런타임)에서 Vue 앱 인스턴스를 만들으며, 라우트에 맞는 페이지 컴포넌트를 서버에서 한 번 렌더링합니다. 이때 useAsyncData·useFetch는 기본적으로 서버에서 데이터를 가져와 직렬화 가능한 페이로드(payload) 로 클라이언트에 넘깁니다. 같은 키로 호출된 데이터는 클라이언트 하이드레이션 시 중복 요청을 피하도록 설계되어 있어, 네트워크 탭에서 API가 두 번 찍히지 않는 것이 정상 동작인 경우가 많습니다.

routeRules로 경로별로 ssr: true를 유지하면서도 캐시 헤더나 ISR에 가까운 동작을 조합할 수 있어, 엣지·CDN과의 연계가 SSR 설계의 핵심이 됩니다.

SSG(정적 사이트 생성)와 프리렌더

nuxt generate(내부적으로 Nitro 프리렌더)는 빌드 타임에 HTML과 자산을 선택아 public·정적 출력 디렉터리에 둡니다. 동적 경로는 nitro.prerender.routes나 크롤링으로 발견된 링크가 있어야 페이지가 생성됩니다. 데이터가 런타임에만 존재하는 API라면, 빌드 시점에 그 API가 접근 가능해야 하며, 그렇지 않으면 빈 페이지나 빌드 실패로 이어집니다. 이는 “SSG가 무조건 저렴하다”가 아니라 데이터 가용성·무효화 전략이 함께 와야 함을 뜻합니다.

CSR(클라이언트 전용)과 SPA 모드

ssr: false(앱 전역 또는 routeRules·페이지 메타)를 쓰면 해당 범위는 브라우저에서만 Vue 앱이 부팅됩니다. SEO가 필요 없는 관리자 콘솔, 지도·WebGL 등 브라우저 API에 강하게 묶인 화면에 적합합니다. 다만 초기 HTML은 거의 비어 있으므로 검색·SNS 크롤러 대응이 약해지으며, 체감 FCP는 번들 다운로드 이후로 밀립니다.

하이브리드와 routeRules의 역할

Nuxt 3는 경로마다 ssr / prerender / headers / redirect 등을 선언해 한 프로젝트 안에서 모드 혼합이 가능합니다. 이는 “프레임워크가 단일 렌더링 모드만 지원한다”는 구식 가정과 달리, 엣지 캐시·오리진·정적 자산을 역할별로 쪼개는 실무 패턴과 맞닿아 있습니다.


10. Auto-imports 메커니즘

Auto-import는 “편의 기능”이 아니라 빌드 파이프라인 상의 계약입니다. Nuxt는 unimport 계열의 해석기로, 설정된 디렉터리(예: components/, composables/, utils/)를 스캔해 이름→심볼 매핑을 만듭니다. 소스 변환 단계에서 실제 import 문이 삽입되거나 가상 모듈 #imports를 통해 노출되며, TypeScript는 별도의 타입 스텁으로 자동 완성이 맞춰집니다.

충돌·우선순위

같은 이름의 composable과 Vue API가 겹치면 의도치 않은 그림자(shadowing) 가 납니다. 팀 규칙으로 use 접두사, 디렉터리 네이밍, 혹은 imports.presets·imports.dirs를 명시해 스캔 순서를 고정하는 편이 안전합니다. 외부 패키지를 자동 등록할 때는 imports 옵션으로 범위를 제한해 번들과 타입 노이즈를 줄입니다.

트리셰이킹과 비용

자동 import가 모든 심볼을 번들에 넣는 것은 아닙니다. 사용된 것만 클라이언트/서버 각각의 롤업 그래프에 들어가도록 설계되어 있으나, 서버 전용 코드가 클라이언트로 새는 실수(예: 비밀을 composable에 하드코딩)는 막지 못합니다. 민감 로직은 server/·runtimeConfig·환경 변수로 분리해야 합니다.


11. Nitro 서버 엔진 아키텍처

Nitro는 Nuxt 3의 서버 층 전부를 담당합니다. 개발 시에는 Vite와 통합된 경량 서버로 동작하며, 프로덕션 빌드에서는 Rollup으로 서버 엔트리·라우트·미들웨어를 하나의 실행 가능한 출력(.output/server/index.mjs 등)으로 묶습니다.

출력물과 프리셋

.output 아래에는 서버 번들, 퍼블릭 자산, 필요 시 Nitro 라우트 매니페스트가 들어갑니다. nitro.preset에 따라 타깃이 달라집니다. 예를 들어 node-server는 Node 프로세스에서 listen하며, cloudflare_pages·vercel 등은 플랫폼이 기대하는 핸들러 형태로 추출됩니다. 즉 “Nuxt를 배포한다”는 것은 동일한 라우트 정의를 다른 런타임 어댑터로 내보낸다는 의미에 가깝습니다.

서버 라우트와 미들웨어 스택

server/api, server/middleware, server/routes의 파일 기반 라우팅은 빌드 시 단일 라우터 테이블로 합쳐집니다. defineEventHandler는 Web 표준 Request/Response에 가까운 H3 이벤트 모델 위에서 동작하며, $fetch는 서버·클라이언트 모두에서 동일한 API로 내부 호출을 최적화(같은 프로세스 내 직접 호출 등)할 수 있습니다.

캐시·스토리지·드라이버

Nitro는 경로별 캐시, 스토리지 추상화, 일부 프리셋에서의 에지 키-값 연계를 제공합니다. 프로덕션에서는 “메모리 캐시가 인스턴스마다 따로다” 같은 수평 확장 가정을 항상 염두에 두어야 합니다.


12. Nuxt 모듈과 훅 시스템

nuxt.configmodules 배열에 등록된 모듈은 빌드 초기화 시점에 로드되어 Nuxt의 내부 라이프사이클에 훅을 겁니다. defineNuxtModule로 감싼 모듈은 스키마 검증(meta.configKey 등)과 함께 설치되어, 플러그인·컴포넌트 자동 등록·라우트 주입·Nitro 설정 병합을 한 번에 처리할 수 있습니다.

자주 쓰는 확장 포인트

  • addPlugin / addComponent / addImports: 클라이언트·서버 공통 초기화와 DX 향상.
  • addServerHandler / addRouteMiddleware: API·미들웨어를 코드로 등록(파일 기반과 병행 가능).
  • nitro 설정 훅: CORS, 압축, 라우트 규칙, 프리렌더 대상을 프로그래밍 방식으로 조정.

nuxt.hooks와 Nitro 훅

Nuxt 훅(hooks: { 'build:before': ... } 등)은 번들링·준비 단계에 개입하며, Nitro 훅은 서버 빌드·프리렌더에 개입합니다. 디버깅 시 “왜 이 라우트가 출력에 없지?”는 어느 훅 단계에서 빠졌는지 역추적하는 것이 빠릅니다.

커스텀 모듈을 작성할 때는 부수 효과 최소화옵션 스키마 명시가 유지보수 비용을 좌우합니다. 사내 모듈은 내부 패키지로 버전을 올리며, 앱 여러 개에 재사용하는 경우 peer 의존성을 문서화하는 것이 좋습니다.


13. 프로덕션 Nuxt 패턴

런타임 설정과 비밀

runtimeConfig는 빌드 시 공개/비공개 키가 나뉘어 주입됩니다. API 키·DB URL 등은 서버 전용 키에만 두며, 클라이언트에 노출되는 public에는 브라우저에 줘도 되는 값만 넣습니다. .env는 배포 플랫폼의 시크릿과 동기화하며, 로컬과 프로덕션의 키 이름 불일치로 인한 장애를 방지합니다.

오류·로깅·헬스

error.vue, 전역 에러 훅, 서버 라우트의 createError일관된 HTTP 상태와 메시지를 반환합니다. 로그는 구조화(JSON)로 남기으며, GET /health 같은 가벼운 헬스 체크를 Nitro 라우트로 두면 오케스트레이터와 로드밸런서가 안정적으로 판단합니다. PM2·컨테이너 재시작 정책과 맞출 때는 무중단 배포 전략(블루/그린, 롤링)과 세션 저장소를 함께 설계합니다.

캐시·성능·자산

정적 자산은 CDN 캐시 헤더와 파일명 해시에 의존하며, HTML/API는 routeRules·역프록시 캐시·애플리케이션 캐시를 계층화합니다. useAsyncDatagetCachedData·키 전략으로 서버 렌더 단계의 중복 페치를 줄이며, 이미지·폰트는 nuxt/image 등으로 포맷·크기 최적화를 자동화합니다.

관측 가능성

프로덕션에서는 Core Web Vitals, 서버 지표(지연, 오류율), Nitro 라우트별 지연을 함께 봅니다. Nuxt 자체보다 배포 프리셋과 런타임(콜드 스타트, CPU 크레딧, 지역)이 병목인 경우가 많으므로, 성능 이슈는 프론트 번들 크기와 서버/엣지 설정을 동시에 의심하는 것이 좋습니다.


정리 및 체크리스트

핵심 요약

  • Nuxt 3: Vue 3 풀스택 프레임워크
  • Auto-imports: unimport 기반 스캔·가상 모듈·타입 스텁
  • Nitro: Rollup 서버 번들, 프리셋별 출력, H3 이벤트 모델
  • useFetch / useAsyncData: SSR 시 페이로드 직렬화와 클라이언트 중복 제거
  • Server Routes: 파일 기반 API가 단일 라우터 테이블로 합쳐짐
  • SSR / SSG / CSR: routeRules·ssr: false로 경로별 하이브리드
  • 모듈·훅: 빌드/서버 단계 확장과 팀 표준화

구현 체크리스트

  • Nuxt 3 프로젝트 생성
  • 페이지 라우팅 구현
  • useFetch로 데이터 페칭
  • Server Routes 작성
  • Middleware 구현
  • Layouts 설정
  • 렌더링 모드(routeRules·SSG 대상 경로) 확정
  • runtimeConfig로 비밀·공개 설정 분리
  • 배포

같이 보면 좋은 글


이 글에서 다루는 키워드

Nuxt, Vue, Full Stack, SSR, Nitro, Web Framework, TypeScript

자주 묻는 질문 (FAQ)

Q. Nuxt 2 vs Nuxt 3, 어떤 게 나은가요?

A. Nuxt 3가 훨씬 빠르고 현대적입니다. 새 프로젝트는 Nuxt 3를 사용하세요.

Q. Next.js vs Nuxt, 어떤 게 나은가요?

A. Vue를 선호하면 Nuxt, React를 선호하면 Next.js를 사용하세요. 기능은 비슷합니다.

Q. Vite를 사용하나요?

A. 네, Nuxt 3는 Vite를 기본으로 사용합니다.

Q. 프로덕션에서 사용해도 되나요?

A. 네. 다만 runtimeConfig로 비밀을 분리하며, routeRules로 캐시·렌더 모드를 경로별로 고정하며, 헬스 체크·로그·배포 프리셋(콜드 스타트·리전)까지 함께 설계하는 것이 운영 안정성에 직결됩니다.