💻 프로그래밍

Nuxt.js

The Intuitive Vue Framework

Vue.js 기반 풀스택 웹 프레임워크. SSR/SSG/ISR 지원. 파일 기반 라우팅. SEO 최적화와 뛰어난 개발자 경험 제공.

📖 상세 설명

Nuxt.js는 Vue.js를 기반으로 한 메타 프레임워크(Meta Framework)로, 웹 애플리케이션 개발에 필요한 설정과 구조를 자동으로 제공합니다. React의 Next.js처럼 Vue 생태계에서 풀스택 개발을 위한 사실상의 표준 프레임워크로 자리잡았으며, 복잡한 웹팩 설정 없이 즉시 프로덕션 수준의 애플리케이션을 구축할 수 있습니다.

Nuxt 3는 SSR(Server-Side Rendering), SSG(Static Site Generation), ISR(Incremental Static Regeneration)을 모두 지원하는 하이브리드 렌더링을 제공합니다. 페이지별로 렌더링 전략을 다르게 설정할 수 있어, 블로그는 SSG로 빌드하고 대시보드는 SSR로 서빙하는 등 유연한 구성이 가능합니다. Nitro 엔진을 통해 엣지 런타임에서도 동작합니다.

파일 기반 라우팅은 Nuxt의 핵심 기능입니다. pages/ 디렉토리에 Vue 파일을 생성하면 자동으로 라우트가 등록됩니다. [id].vue 형태로 동적 라우팅을, [...slug].vue로 Catch-all 라우팅을 구현합니다. 또한 layouts/, middleware/, composables/ 등의 디렉토리 구조를 통해 코드 조직화를 강제하여 일관된 프로젝트 아키텍처를 유지할 수 있습니다.

실무에서 Nuxt는 SEO가 중요한 마케팅 사이트, 블로그, 이커머스에 특히 강점을 발휘합니다. 서버에서 완성된 HTML을 전달하므로 검색 엔진 크롤러가 콘텐츠를 쉽게 인덱싱하고, 초기 로딩 속도(FCP, LCP)가 CSR 대비 현저히 빠릅니다. useFetch, useAsyncData 등의 Composable로 데이터 페칭과 캐싱을 선언적으로 관리할 수 있어 개발 생산성도 높습니다.

📊 렌더링 모드 비교

모드 빌드 시점 SEO 사용 사례
SSR 요청마다 서버에서 렌더링 우수 실시간 데이터, 개인화 페이지
SSG 빌드 시 정적 HTML 생성 우수 블로그, 문서, 랜딩 페이지
ISR 빌드 + 주기적 재생성 우수 상품 목록, 뉴스 (자주 변경)
CSR 클라이언트에서 렌더링 제한적 인증 후 대시보드, 관리자 페이지

실무 팁: nuxt.config.ts에서 routeRules를 사용해 경로별로 렌더링 모드를 지정할 수 있습니다. 예: '/blog/**': { prerender: true }

💻 코드 예제

<!-- pages/posts/[id].vue - 동적 라우팅 페이지 -->
<script setup lang="ts">
// 라우트 파라미터 접근
const route = useRoute()
const postId = route.params.id

// 서버에서 데이터 페칭 (SSR/SSG 지원)
const { data: post, pending, error } = await useFetch(
  `/api/posts/${postId}`,
  {
    // 캐싱 전략 설정
    key: `post-${postId}`,
    // 에러 발생 시 404 페이지로 리다이렉트
    onResponseError({ response }) {
      if (response.status === 404) {
        throw createError({
          statusCode: 404,
          message: '포스트를 찾을 수 없습니다'
        })
      }
    }
  }
)

// SEO 메타 태그 설정
useSeoMeta({
  title: () => post.value?.title || '로딩 중...',
  description: () => post.value?.excerpt,
  ogImage: () => post.value?.thumbnail
})
</script>

<template>
  <div class="post-page">
    <!-- 로딩 상태 -->
    <div v-if="pending" class="loading">
      <LoadingSpinner />
    </div>

    <!-- 에러 상태 -->
    <div v-else-if="error" class="error">
      <p>{{ error.message }}</p>
      <NuxtLink to="/posts">목록으로 돌아가기</NuxtLink>
    </div>

    <!-- 성공 상태 -->
    <article v-else-if="post">
      <h1>{{ post.title }}</h1>
      <time>{{ formatDate(post.createdAt) }}</time>
      <div class="content" v-html="post.content" />
    </article>
  </div>
</template>
// composables/useProducts.ts - 재사용 가능한 데이터 로직
export const useProducts = () => {
  // 상품 목록 조회 (캐싱 적용)
  const fetchProducts = async (category?: string) => {
    return useFetch('/api/products', {
      query: { category },
      // 5분간 캐시 유지
      getCachedData(key) {
        const data = nuxtApp.payload.data[key]
        if (!data) return null

        const expirationDate = new Date(data.fetchedAt)
        expirationDate.setMinutes(expirationDate.getMinutes() + 5)

        if (expirationDate < new Date()) return null
        return data
      },
      transform(data) {
        return { ...data, fetchedAt: new Date() }
      }
    })
  }

  // 단일 상품 조회
  const fetchProduct = async (id: string) => {
    return useFetch(`/api/products/${id}`, {
      key: `product-${id}`,
      // Nuxt 3의 자동 dedupe - 같은 요청 중복 방지
      dedupe: 'defer'
    })
  }

  // 상품 검색 (클라이언트 사이드)
  const searchProducts = async (query: string) => {
    // $fetch는 useFetch와 달리 반응형이 아님
    // 이벤트 핸들러에서 사용
    return $fetch('/api/products/search', {
      method: 'POST',
      body: { query }
    })
  }

  return {
    fetchProducts,
    fetchProduct,
    searchProducts
  }
}

// 사용 예시: pages/products/index.vue
<script setup>
const { fetchProducts } = useProducts()
const route = useRoute()

// URL 쿼리 파라미터로 카테고리 필터링
const { data: products, refresh } = await fetchProducts(
  route.query.category as string
)

// 카테고리 변경 시 자동 리프레시
watch(() => route.query.category, () => refresh())
</script>
// nuxt.config.ts - 프로젝트 설정
export default defineNuxtConfig({
  // TypeScript 지원
  typescript: {
    strict: true
  },

  // 런타임 환경 변수
  runtimeConfig: {
    // 서버 전용 (절대 클라이언트에 노출 안됨)
    apiSecret: process.env.API_SECRET,
    // 클라이언트에도 노출
    public: {
      apiBase: process.env.API_BASE_URL || 'https://api.example.com'
    }
  },

  // 모듈 (플러그인 생태계)
  modules: [
    '@nuxt/ui',           // 공식 UI 컴포넌트
    '@nuxt/image',        // 이미지 최적화
    '@pinia/nuxt',        // 상태 관리
    '@nuxtjs/i18n',       // 다국어 지원
    '@vueuse/nuxt'        // Vue Composables
  ],

  // 하이브리드 렌더링 - 경로별 렌더링 전략
  routeRules: {
    // 홈페이지: ISR (60초마다 재생성)
    '/': { isr: 60 },
    // 블로그: 정적 생성
    '/blog/**': { prerender: true },
    // 대시보드: CSR only (서버 렌더링 비활성화)
    '/dashboard/**': { ssr: false },
    // API 프록시
    '/api/**': {
      proxy: 'https://api.backend.com/**',
      cors: true
    }
  },

  // Nitro 서버 설정
  nitro: {
    // Vercel, Netlify, Cloudflare 등 자동 감지
    preset: 'vercel-edge',
    // 서버 캐싱
    storage: {
      cache: {
        driver: 'redis',
        url: process.env.REDIS_URL
      }
    }
  },

  // 앱 전역 설정
  app: {
    head: {
      title: 'My Nuxt App',
      meta: [
        { name: 'viewport', content: 'width=device-width, initial-scale=1' }
      ],
      link: [
        { rel: 'icon', type: 'image/png', href: '/favicon.png' }
      ]
    }
  }
})

🗣️ 실무 대화 예시

신규 프로젝트 기술 스택 선정 회의에서

"SEO가 중요한 마케팅 사이트니까 CSR로는 한계가 있어요. Nuxt로 가면 SSG로 빌드해서 Vercel에 배포하면 되고, 나중에 동적 기능 추가할 때도 SSR로 전환이 쉽습니다. 순수 Vue보다 초기 설정 시간도 절약되고, 파일 기반 라우팅으로 팀원들 온보딩도 빨라요."

CSR vs SSR 렌더링 방식 논의 중

"대시보드 같은 인증 후 페이지는 굳이 SSR 안 해도 됩니다. nuxt.config에서 routeRules로 '/dashboard/**'만 ssr: false 하면 돼요. 근데 랜딩 페이지나 상품 상세는 SEO랑 초기 로딩 속도 때문에 SSR이 필수입니다. Nuxt는 이런 하이브리드 설정이 간단해서 좋아요."

기술 면접에서

"Nuxt가 Vue 위에 제공하는 핵심 가치는 세 가지입니다. 첫째, 유니버설 렌더링으로 SEO와 성능을 모두 잡고, 둘째, 파일 시스템 기반 라우팅과 디렉토리 컨벤션으로 프로젝트 구조를 표준화하며, 셋째, Nitro 엔진을 통해 엣지 런타임부터 Node.js까지 다양한 배포 환경을 단일 코드베이스로 지원합니다."

코드 리뷰에서 - SSR 데이터 페칭

"이 컴포넌트에서 onMounted 안에서 API 호출하고 있는데, 그러면 SSR 때는 데이터가 없어서 SEO에 불리해요. useFetch나 useAsyncData를 setup 레벨에서 호출하면 서버에서 데이터를 가져와서 HTML에 포함시킵니다. 그리고 key 옵션을 꼭 넣어서 캐싱을 활용하세요. 같은 키면 요청이 중복되지 않아요."

⚠️ 주의사항

1
서버 사이드 상태 관리

SSR에서 전역 상태(Pinia store)를 부주의하게 사용하면 요청 간 상태가 공유되어 보안 문제가 발생합니다. useState() Composable을 사용하거나, Pinia에서 상태를 함수로 초기화해야 합니다. 특히 인증 토큰 같은 민감 정보는 쿠키나 헤더로 관리하세요.

2
Hydration Mismatch

서버에서 렌더링된 HTML과 클라이언트에서 생성된 DOM이 다르면 Hydration Mismatch 경고가 발생합니다. 흔한 원인: Date.now(), Math.random() 사용, 브라우저 전용 API 접근. <ClientOnly> 컴포넌트로 감싸거나 onMounted에서 처리하세요.

3
배포 환경 고려

SSR은 Node.js 서버가 필요하고 SSG는 정적 호스팅으로 충분합니다. Vercel/Netlify는 자동 설정되지만, 자체 서버 배포 시 Nitro preset을 맞게 설정해야 합니다. 엣지 런타임(Cloudflare Workers)은 Node.js API 일부가 지원 안 될 수 있으니 확인이 필요합니다.

🔗 관련 용어

📚 더 배우기