Vue 완벽 마스터

Vue의 핵심 개념과 실전 활용법

Vue중급
10시간
5개 항목
학습 진행률0 / 5 (0%)

학습 항목

1. Vue
Nuxt3|풀스택|개발|완벽|가이드
퀴즈튜토리얼
2. Vue
Pinia|상태관리|Vue3|완벽|가이드
퀴즈튜토리얼
3. Vue
고급
Vue.js|실전|프로젝트|완벽|가이드
퀴즈튜토리얼
4. Vue
Vue3|Composition|API|완벽|가이드
퀴즈튜토리얼
5. Vue
Vue|컴포넌트|설계|패턴|완벽|가이드
퀴즈튜토리얼
1 / 5

이미지 로딩 중...

Nuxt3 풀스택 개발 완벽 가이드 - 슬라이드 1/13

Nuxt3 풀스택 개발 완벽 가이드

Nuxt3를 사용한 풀스택 웹 애플리케이션 개발의 모든 것을 다룹니다. 서버 사이드 렌더링부터 API 개발, 데이터베이스 연동, 배포까지 실무에 필요한 핵심 개념과 베스트 프랙티스를 상세하게 설명합니다.


목차

  1. Nuxt3 프로젝트 구조와 Auto Imports
  2. Server Routes와 API 개발
  3. useFetch와 데이터 페칭
  4. Middleware와 라우트 가드
  5. Nuxt Layouts과 Pages
  6. Server-Side Rendering (SSR) vs Static Generation
  7. Composables로 로직 재사용
  8. Nuxt Config와 환경 설정
  9. Nitro 엔진과 서버 미들웨어
  10. 프로덕션 배포와 최적화

1. Nuxt3 프로젝트 구조와 Auto Imports

시작하며

여러분이 Vue 3 프로젝트를 개발할 때, 매번 import 문을 수십 번씩 작성하고 관리하느라 지친 적 있나요? 컴포넌트를 사용할 때마다 import를 추가하고, composable을 쓸 때마다 경로를 찾아 헤매고, 나중에 코드를 정리할 때 사용하지 않는 import를 찾아 삭제하는 작업까지.

이런 번거로운 작업들은 개발 생산성을 크게 떨어뜨립니다. 특히 대규모 프로젝트에서는 import 관리만으로도 상당한 시간이 소요되며, 실수로 잘못된 경로를 입력하거나 순환 참조 문제가 발생하기도 합니다.

바로 이럴 때 필요한 것이 Nuxt3의 Auto Imports 시스템입니다. Nuxt3는 프로젝트 구조를 기반으로 컴포넌트, composables, utils를 자동으로 인식하고 임포트해주어 개발자가 비즈니스 로직에만 집중할 수 있게 해줍니다.

개요

간단히 말해서, Nuxt3의 Auto Imports는 특정 디렉토리에 파일을 배치하면 별도의 import 문 없이 자동으로 사용할 수 있게 해주는 시스템입니다. 왜 이 기능이 필요한지 실무 관점에서 설명하면, 대규모 프로젝트에서 수백 개의 컴포넌트와 유틸리티 함수를 관리할 때 import 관리는 정말 골치 아픈 일입니다.

예를 들어, 여러 페이지에서 사용하는 공통 컴포넌트를 수정하거나 이동할 때마다 모든 import 경로를 수정해야 하는 경우가 그렇습니다. 기존 Vue 3 프로젝트에서는 매번 import { ref, computed } from 'vue'import MyComponent from '@/components/MyComponent.vue'처럼 명시적으로 작성해야 했다면, Nuxt3에서는 그냥 바로 사용할 수 있습니다.

Nuxt3는 components/, composables/, utils/ 디렉토리를 자동으로 스캔하며, Vue의 모든 핵심 API(ref, computed, watch 등)도 자동으로 사용 가능하고, TypeScript 타입 지원까지 완벽하게 제공합니다. 이러한 특징들이 개발 경험을 크게 향상시키고 코드의 가독성을 높여줍니다.

코드 예제

// components/UserCard.vue - 자동으로 전역에서 사용 가능
<template>
  <div class="user-card">
    <h3>{{ user.name }}</h3>
    <p>{{ user.email }}</p>
  </div>
</template>

<script setup lang="ts">
// import 없이 바로 사용 가능
const props = defineProps<{
  user: { name: string; email: string }
}>()

// ref, computed 등도 자동 import
const isActive = ref(true)
const displayName = computed(() => props.user.name.toUpperCase())
</script>

설명

이것이 하는 일: Nuxt3의 Auto Imports 시스템은 프로젝트의 특정 디렉토리를 스캔하여 파일들을 자동으로 등록하고, 코드 어디서든 import 문 없이 사용할 수 있게 해줍니다. 첫 번째로, components/ 디렉토리에 파일을 생성하면 자동으로 전역 컴포넌트로 등록됩니다.

위 코드에서 UserCard.vue를 만들면, 다른 어떤 페이지나 컴포넌트에서도 <UserCard :user="userData" />처럼 바로 사용할 수 있습니다. 왜 이렇게 하는지는, 컴포넌트를 사용할 때마다 import 경로를 찾고 작성하는 번거로움을 없애기 위함입니다.

그 다음으로, <script setup> 내부에서 Vue의 핵심 API들이 실행되면서 자동으로 주입됩니다. ref, computed, watch 같은 함수들을 import 없이 바로 사용할 수 있으며, Nuxt는 빌드 타임에 실제로 사용된 것들만 번들에 포함시킵니다.

내부적으로는 Vite의 플러그인 시스템과 TypeScript의 타입 선언을 활용하여 이를 구현합니다. 마지막으로, TypeScript 타입 체킹이 완벽하게 작동하여 자동완성과 타입 안정성을 모두 제공합니다.

Nuxt는 .nuxt/types/ 디렉토리에 자동으로 타입 선언 파일을 생성하므로, IDE에서 코드 작성 시 모든 자동 임포트된 항목들에 대한 인텔리센스를 받을 수 있습니다. 여러분이 이 기능을 사용하면 코드가 훨씬 깔끔해지고, 개발 속도가 30-50% 이상 향상되며, import 관리로 인한 버그를 원천적으로 방지할 수 있습니다.

또한 컴포넌트 리팩토링이나 파일 이동 시에도 import 경로를 수정할 필요가 없어 유지보수가 매우 쉬워집니다.

실전 팁

💡 components/ 디렉토리 내에 하위 폴더를 만들면 컴포넌트 이름이 자동으로 네임스페이스화됩니다. 예를 들어 components/user/Card.vue<UserCard />로 사용됩니다. 이를 활용해 컴포넌트를 체계적으로 분류하세요.

💡 자동 임포트를 비활성화하고 싶은 파일이 있다면 nuxt.config.ts에서 components.dirs를 커스터마이징하거나, 파일명 앞에 언더스코어(_helper.ts)를 붙여 명시적으로 import해서 사용할 수 있습니다.

💡 composables/ 디렉토리의 함수는 반드시 use로 시작하는 네이밍 컨벤션을 따르세요. 예: useAuth(), useFetch(). 이는 Vue의 Composition API 규칙이며, 자동 임포트가 더 잘 작동합니다.

💡 개발 중에 자동 임포트가 제대로 인식되지 않으면 .nuxt 폴더를 삭제하고 개발 서버를 재시작하세요. Nuxt가 타입 정의를 다시 생성하면서 문제가 해결됩니다.

💡 성능 최적화를 위해 사용하지 않는 컴포넌트는 자동으로 번들에서 제외됩니다. Tree-shaking이 자동으로 적용되므로 걱정하지 않고 많은 컴포넌트를 만들어도 됩니다.


2. Server Routes와 API 개발

시작하며

여러분이 프론트엔드와 백엔드를 따로 개발하면서 CORS 설정, 개발 환경 프록시 구성, 별도의 서버 프로젝트 관리로 힘들었던 경험이 있나요? 프론트엔드 개발자가 간단한 API를 추가하려고 해도 백엔드 프로젝트를 별도로 설정하고, 다른 언어와 프레임워크를 배워야 하는 상황.

이런 문제는 특히 작은 팀이나 풀스택 개발자에게 큰 부담입니다. 두 개의 서로 다른 프로젝트를 동시에 관리하고, 배포 파이프라인도 따로 구축해야 하며, API 스펙이 변경될 때마다 프론트엔드와 백엔드를 모두 수정해야 합니다.

바로 이럴 때 필요한 것이 Nuxt3의 Server Routes입니다. 같은 프로젝트 내에서 TypeScript로 API를 개발하고, 자동으로 타입 안정성을 갖추며, 하나의 배포로 풀스택 애플리케이션을 완성할 수 있습니다.

개요

간단히 말해서, Server Routes는 Nuxt3 프로젝트의 server/api/ 디렉토리에 파일을 만들면 자동으로 API 엔드포인트가 생성되는 시스템입니다. 왜 이 기능이 필요한지 실무 관점에서 설명하면, 많은 웹 애플리케이션이 간단한 CRUD API만 필요한 경우가 많습니다.

예를 들어, 사용자 프로필을 조회하거나 설정을 저장하는 API 같은 경우, 별도의 복잡한 백엔드 서버를 구축하는 것은 과도한 엔지니어링입니다. 기존에는 Express.js나 Fastify 같은 별도의 Node.js 서버를 구축하고, API 라우팅을 수동으로 설정하고, 타입 정의를 프론트엔드와 따로 관리해야 했다면, Nuxt3에서는 파일 하나만 만들면 모든 것이 자동으로 처리됩니다.

Server Routes는 Nitro 엔진 위에서 동작하여 매우 빠르고, 파일 기반 라우팅으로 직관적이며, TypeScript 타입이 프론트엔드와 자동으로 공유되고, H3라는 경량 HTTP 프레임워크를 사용하여 Express보다 훨씬 빠릅니다. 이러한 특징들이 개발 생산성과 애플리케이션 성능을 동시에 향상시킵니다.

코드 예제

// server/api/users/[id].get.ts - GET /api/users/:id 엔드포인트
export default defineEventHandler(async (event) => {
  // URL 파라미터 추출
  const id = getRouterParam(event, 'id')

  // 쿼리 파라미터 추출
  const { include } = getQuery(event)

  // 데이터베이스 조회 (예시)
  const user = await prisma.user.findUnique({
    where: { id },
    include: include === 'posts' ? { posts: true } : undefined
  })

  if (!user) {
    throw createError({ statusCode: 404, message: 'User not found' })
  }

  return user
})

설명

이것이 하는 일: Server Routes는 파일 시스템 기반으로 API 엔드포인트를 자동 생성하고, HTTP 요청을 처리하며, 프론트엔드와 동일한 TypeScript 환경에서 백엔드 로직을 작성할 수 있게 해줍니다. 첫 번째로, 파일명이 곧 API 경로가 됩니다.

server/api/users/[id].get.tsGET /api/users/:id 엔드포인트로 자동 매핑됩니다. 대괄호 [id]는 동적 파라미터를 의미하며, .get, .post, .put, .delete 확장자로 HTTP 메서드를 지정합니다.

이렇게 하는 이유는 명시적인 라우팅 설정 없이도 파일 구조만으로 API를 이해할 수 있게 하기 위함입니다. 그 다음으로, defineEventHandler 함수로 요청 핸들러를 정의합니다.

event 객체를 통해 요청의 모든 정보에 접근할 수 있으며, getRouterParam으로 URL 파라미터를, getQuery로 쿼리 스트링을, readBody로 요청 본문을 읽을 수 있습니다. 내부적으로는 H3 프레임워크가 이 모든 유틸리티를 제공하며, 타입 안정성이 보장됩니다.

마지막으로, 에러 처리와 응답 생성이 매우 간단합니다. createError로 HTTP 에러를 던지면 자동으로 적절한 상태 코드와 메시지가 클라이언트에 전달되고, 객체를 return하면 자동으로 JSON으로 직렬화되어 응답됩니다.

Prisma, Drizzle 같은 ORM이나 데이터베이스 클라이언트를 직접 사용할 수 있습니다. 여러분이 이 기능을 사용하면 하나의 프로젝트로 풀스택 개발이 가능하고, 프론트엔드와 백엔드 간 타입 공유로 런타임 에러가 크게 줄어들며, 배포가 단순해지고, API 개발 속도가 기존 대비 2-3배 빨라집니다.

특히 프로토타입 개발이나 MVP 제작 시 엄청난 시간을 절약할 수 있습니다.

실전 팁

💡 미들웨어를 사용해 인증을 구현하려면 server/middleware/auth.ts에 작성하세요. 모든 API 요청 전에 자동으로 실행되어 JWT 토큰 검증 등을 처리할 수 있습니다.

💡 API 응답 캐싱을 위해 defineCachedEventHandler를 사용하세요. 데이터베이스 조회 결과를 메모리나 Redis에 캐싱하여 성능을 크게 향상시킬 수 있습니다.

💡 개발 환경에서는 .env 파일의 환경 변수가 자동으로 로드되지만, 프로덕션에서는 runtimeConfig를 사용하세요. nuxt.config.ts에서 정의하면 타입 안전하게 사용할 수 있습니다.

💡 복잡한 비즈니스 로직은 server/utils/에 별도 함수로 분리하세요. 재사용성이 높아지고 테스트하기 쉬워집니다. 예: server/utils/emailService.ts

💡 API 성능 모니터링을 위해 event.context에 타이밍 정보를 기록하세요. 미들웨어에서 시작 시간을 기록하고 핸들러에서 종료 시간을 로깅하면 병목을 쉽게 찾을 수 있습니다.


3. useFetch와 데이터 페칭

시작하며

여러분이 Vue 애플리케이션에서 데이터를 가져올 때, 로딩 상태 관리, 에러 처리, SSR과 CSR의 이중 페칭 문제, SEO를 위한 서버 데이터 전달 등으로 복잡한 코드를 작성한 경험이 있나요? axios나 fetch를 사용하면서 mounted 훅에서 데이터를 가져오고, 로딩 스피너를 수동으로 관리하고, try-catch로 에러를 처리하는 반복적인 패턴.

이런 문제는 특히 SSR 환경에서 더 복잡해집니다. 서버에서 한 번 데이터를 가져왔는데 클라이언트에서 또 가져오는 이중 요청이 발생하거나, 서버에서 가져온 데이터를 클라이언트로 제대로 전달하지 못해 깜빡임이 발생하기도 합니다.

또한 SEO를 위해 초기 HTML에 데이터가 포함되어야 하는데 이를 수동으로 구현하기는 매우 어렵습니다. 바로 이럴 때 필요한 것이 Nuxt3의 useFetch입니다.

SSR과 CSR 모두에서 완벽하게 작동하며, 자동으로 중복 요청을 방지하고, 반응형 상태 관리를 제공하여 데이터 페칭의 모든 복잡함을 해결해줍니다.

개요

간단히 말해서, useFetch는 Nuxt3에서 제공하는 composable로, API 호출을 매우 간단하게 만들어주고 SSR 환경에서 최적화된 데이터 페칭을 자동으로 처리합니다. 왜 이 기능이 필요한지 실무 관점에서 설명하면, 대부분의 웹 애플리케이션은 외부 API나 자체 백엔드에서 데이터를 가져와 화면에 표시합니다.

예를 들어, 사용자 대시보드를 렌더링할 때 사용자 정보, 최근 활동, 알림 등 여러 API를 동시에 호출하고 관리해야 하는 경우가 그렇습니다. 기존 방식에서는 onMounted에서 await fetch()를 호출하고, reactive 변수에 할당하고, 로딩과 에러 상태를 별도로 관리해야 했다면, useFetch는 이 모든 것을 한 줄로 해결합니다.

useFetch는 서버와 클라이언트에서 자동으로 작동하고, 페이로드 최적화로 서버 데이터를 효율적으로 전달하며, 자동 재시도와 에러 처리를 내장하고, TypeScript 제네릭으로 완벽한 타입 추론을 제공합니다. 이러한 특징들이 개발자 경험을 크게 향상시키고 애플리케이션 성능과 SEO를 동시에 개선합니다.

코드 예제

// pages/users/[id].vue - 사용자 상세 페이지
<script setup lang="ts">
// URL 파라미터에서 id 가져오기
const route = useRoute()
const userId = route.params.id

// API 호출 - 자동으로 SSR에서 실행되고 클라이언트로 전달됨
const { data: user, pending, error, refresh } = await useFetch(`/api/users/${userId}`, {
  // 타입 지정
  key: `user-${userId}`,
  // 쿼리 파라미터
  query: { include: 'posts' },
  // 데이터 변환
  transform: (data) => data.user
})

// 수동으로 새로고침 가능
const handleRefresh = () => refresh()
</script>

<template>
  <div v-if="pending">로딩 중...</div>
  <div v-else-if="error">에러: {{ error.message }}</div>
  <div v-else-if="user">
    <h1>{{ user.name }}</h1>
    <button @click="handleRefresh">새로고침</button>
  </div>
</template>

설명

이것이 하는 일: useFetch는 서버 사이드와 클라이언트 사이드에서 모두 API 요청을 처리하고, 응답 데이터를 반응형 상태로 관리하며, 서버에서 가져온 데이터를 자동으로 클라이언트로 전달하여 이중 요청을 방지합니다. 첫 번째로, await useFetch()를 호출하면 컴포넌트가 렌더링되기 전에 데이터를 가져옵니다.

SSR 환경에서는 서버에서 API를 호출하고 그 결과를 HTML과 함께 클라이언트로 전달합니다. 클라이언트에서는 이미 받은 데이터를 재사용하므로 같은 요청을 다시 보내지 않습니다.

이렇게 하는 이유는 불필요한 네트워크 요청을 줄이고 초기 렌더링 속도를 높이기 위함입니다. 그 다음으로, 반환되는 객체에서 data, pending, error, refresh 같은 반응형 상태를 구조 분해로 받습니다.

data는 API 응답 데이터이고, pending은 로딩 중 여부, error는 발생한 에러, refresh는 수동으로 재요청하는 함수입니다. 내부적으로 이들은 모두 ref로 관리되므로 값이 변경되면 자동으로 UI가 업데이트됩니다.

마지막으로, key 옵션으로 캐시 키를 지정하고, transform으로 응답 데이터를 가공하고, query로 쿼리 파라미터를 전달할 수 있습니다. key는 같은 데이터를 여러 곳에서 공유할 때 중요하며, Nuxt가 내부 캐시를 관리하는 기준이 됩니다.

transform은 서버에서 실행되어 불필요한 데이터를 제거하고 페이로드 크기를 줄일 수 있습니다. 여러분이 이 기능을 사용하면 데이터 페칭 코드가 80% 이상 줄어들고, SSR과 CSR을 별도로 처리할 필요가 없으며, 자동으로 SEO 최적화가 되고, 사용자는 더 빠른 페이지 로딩을 경험합니다.

특히 검색 엔진이 완전히 렌더링된 HTML을 받을 수 있어 SEO 점수가 크게 향상됩니다.

실전 팁

💡 여러 API를 동시에 호출할 때는 Promise.all 대신 여러 개의 useFetch를 병렬로 사용하세요. Nuxt가 자동으로 병렬 처리하고 모든 요청이 완료될 때까지 기다립니다.

💡 lazy: true 옵션을 사용하면 컴포넌트 렌더링을 차단하지 않고 백그라운드에서 데이터를 가져옵니다. 중요하지 않은 데이터에 유용하며, 이 경우 useLazyFetch를 직접 사용할 수도 있습니다.

💡 watch 옵션으로 반응형 변수를 감시하여 자동으로 재요청할 수 있습니다. 예: watch: [searchQuery]로 설정하면 검색어가 바뀔 때마다 자동으로 API를 다시 호출합니다.

💡 내부 API(/api/*)를 호출할 때는 상대 경로를 사용하고, 외부 API는 절대 URL을 사용하세요. 내부 API는 SSR 시 서버 내부에서 직접 호출되어 네트워크 오버헤드가 없습니다.

💡 getCachedData 옵션으로 커스텀 캐싱 전략을 구현할 수 있습니다. 예를 들어 localStorage나 IndexedDB에서 캐시된 데이터를 먼저 확인하고, 없을 때만 API를 호출하도록 할 수 있습니다.


4. Middleware와 라우트 가드

시작하며

여러분이 웹 애플리케이션을 개발하면서 로그인하지 않은 사용자가 대시보드에 접근하거나, 권한이 없는 사용자가 관리자 페이지를 보려고 시도하는 것을 막아야 했던 경험이 있나요? 각 페이지 컴포넌트에서 onMountedbeforeRouteEnter로 인증 상태를 확인하고, 조건에 맞지 않으면 리다이렉트하는 코드를 반복적으로 작성하는 상황.

이런 문제는 코드 중복을 발생시키고 보안 취약점을 만들 수 있습니다. 특히 새로운 페이지를 추가할 때마다 인증 로직을 빠뜨리기 쉽고, SSR 환경에서는 서버와 클라이언트 양쪽에서 동일한 검증을 해야 하는 복잡함이 있습니다.

또한 페이지가 렌더링된 후에 리다이렉트되면 사용자가 잠깐 동안 보안이 필요한 콘텐츠를 볼 수 있는 문제도 발생합니다. 바로 이럼 때 필요한 것이 Nuxt3의 Middleware 시스템입니다.

라우트 이동 전에 자동으로 실행되어 인증, 권한 체크, 로깅, 데이터 사전 로드 등을 중앙에서 관리하고, 서버와 클라이언트 모두에서 작동하여 보안을 강화합니다.

개요

간단히 말해서, Middleware는 페이지로 이동하기 전에 실행되는 함수로, 조건에 따라 접근을 허용하거나 다른 페이지로 리다이렉트할 수 있게 해줍니다. 왜 이 기능이 필요한지 실무 관점에서 설명하면, 대부분의 애플리케이션은 보호되어야 하는 페이지가 있습니다.

예를 들어, 쇼핑몰의 주문 내역, 관리자 대시보드, 사용자 설정 페이지 등은 인증된 사용자만 접근할 수 있어야 하며, 이를 각 페이지에서 개별적으로 처리하는 것은 비효율적이고 위험합니다. 기존 Vue Router에서는 beforeEach 네비게이션 가드를 수동으로 설정하고, 인증 로직을 직접 구현하고, 모든 보호된 라우트를 일일이 명시해야 했다면, Nuxt3에서는 파일 기반으로 미들웨어를 정의하고 필요한 페이지에만 적용하면 됩니다.

Middleware는 글로벌, 명명된, 인라인 세 가지 타입을 지원하고, 서버와 클라이언트 모두에서 실행되며, 비동기 작업을 지원하고, TypeScript로 타입 안전하게 작성할 수 있습니다. 이러한 특징들이 보안을 강화하고 코드 중복을 제거하며 유지보수를 쉽게 만듭니다.

코드 예제

// middleware/auth.ts - 인증 미들웨어
export default defineNuxtRouteMiddleware(async (to, from) => {
  // 인증 상태 확인
  const { status, signIn } = useAuth()

  // 로그인이 필요한 페이지 패턴
  const protectedRoutes = ['/dashboard', '/profile', '/settings']
  const isProtected = protectedRoutes.some(route => to.path.startsWith(route))

  // 인증되지 않은 사용자가 보호된 페이지 접근 시
  if (isProtected && status.value !== 'authenticated') {
    // 로그인 후 원래 페이지로 돌아오도록 설정
    return navigateTo({
      path: '/login',
      query: { redirect: to.fullPath }
    })
  }

  // 이미 로그인한 사용자가 로그인 페이지 접근 시
  if (to.path === '/login' && status.value === 'authenticated') {
    return navigateTo('/dashboard')
  }
})

설명

이것이 하는 일: Middleware는 라우트 네비게이션 과정에서 자동으로 실행되어, 사용자의 인증 상태나 권한을 확인하고, 조건에 맞지 않으면 적절한 페이지로 리다이렉트하여 보안이 필요한 페이지를 보호합니다. 첫 번째로, defineNuxtRouteMiddleware로 미들웨어 함수를 정의하면 tofrom 파라미터를 받습니다.

to는 사용자가 가려는 라우트 정보이고, from은 현재 라우트입니다. 이 정보를 사용해 경로, 쿼리 파라미터, 메타 정보 등을 확인할 수 있습니다.

왜 이렇게 하는지는 라우트 이동 전에 필요한 모든 정보를 제공하여 개발자가 유연하게 조건을 처리할 수 있게 하기 위함입니다. 그 다음으로, useAuth 같은 composable을 사용해 현재 인증 상태를 확인합니다.

실제 프로젝트에서는 JWT 토큰 검증, 세션 확인, 사용자 역할 체크 등 복잡한 로직이 들어갈 수 있으며, 이를 composable로 추상화하면 재사용성이 높아집니다. 비동기 작업도 완벽하게 지원되므로 데이터베이스나 외부 API를 호출할 수도 있습니다.

마지막으로, navigateTo를 return하면 사용자를 다른 페이지로 리다이렉트합니다. 이때 query 파라미터로 원래 가려던 경로를 전달하면, 로그인 후 자동으로 그 페이지로 돌아가는 UX를 구현할 수 있습니다.

아무것도 return하지 않으면 정상적으로 페이지 이동이 진행됩니다. 여러분이 이 기능을 사용하면 보안 로직을 중앙에서 관리하여 누락을 방지하고, 모든 페이지에서 일관된 인증 경험을 제공하며, SSR에서도 서버 측에서 미리 체크하여 보안이 필요한 콘텐츠가 HTML에 포함되지 않도록 방지할 수 있습니다.

또한 새로운 페이지를 추가할 때도 미들웨어만 적용하면 되므로 개발 속도가 빨라집니다.

실전 팁

💡 글로벌 미들웨어는 middleware/ 디렉토리에 .global.ts 확장자로 만드세요. 예: auth.global.ts는 모든 페이지 이동 시 자동으로 실행됩니다.

💡 특정 페이지에만 미들웨어를 적용하려면 페이지 컴포넌트에서 definePageMeta({ middleware: 'auth' })로 명시하세요. 불필요한 실행을 방지하여 성능이 향상됩니다.

💡 미들웨어는 실행 순서가 중요합니다. 글로벌 미들웨어 → 레이아웃 미들웨어 → 페이지 미들웨어 순으로 실행되므로, 이를 고려해 로직을 작성하세요.

💡 복잡한 권한 체크는 RBAC(Role-Based Access Control) 패턴을 사용하세요. 사용자 역할을 배열로 관리하고, 페이지마다 필요한 역할을 메타데이터로 정의하면 유지보수가 쉬워집니다.

💡 개발 환경에서 미들웨어를 임시로 비활성화하려면 if (process.dev) return을 추가하세요. 테스트나 디버깅 시 편리합니다.


5. Nuxt Layouts과 Pages

시작하며

여러분이 여러 페이지를 개발하면서 헤더, 푸터, 사이드바 같은 공통 UI를 매 페이지마다 복사해서 붙여넣고, 나중에 디자인이 변경되면 모든 페이지를 일일이 수정해야 했던 경험이 있나요? 또는 관리자 페이지와 일반 사용자 페이지가 완전히 다른 레이아웃을 가지는데, 이를 관리하기 위해 복잡한 조건문을 사용하거나 별도의 컴포넌트 계층을 만들어야 했던 상황.

이런 문제는 코드 중복을 심화시키고 유지보수를 어렵게 만듭니다. 특히 대규모 애플리케이션에서 수십 개의 페이지가 있을 때, 공통 레이아웃의 작은 변경 하나가 모든 페이지 수정으로 이어질 수 있습니다.

또한 레이아웃 간 전환이 매끄럽지 않거나, 레이아웃별 상태 관리가 복잡해지는 문제도 발생합니다. 바로 이럴 때 필요한 것이 Nuxt3의 Layouts 시스템입니다.

재사용 가능한 레이아웃을 정의하고, 페이지마다 다른 레이아웃을 쉽게 적용하며, 레이아웃 전환 애니메이션까지 자동으로 처리하여 코드 중복을 제거하고 일관된 UI를 유지할 수 있습니다.

개요

간단히 말해서, Layouts는 여러 페이지에서 공유하는 공통 UI 구조를 정의하는 래퍼 컴포넌트이며, Pages는 실제 콘텐츠를 담은 개별 페이지 컴포넌트입니다. 왜 이 기능이 필요한지 실무 관점에서 설명하면, 대부분의 웹 애플리케이션은 여러 페이지에서 반복되는 UI 패턴이 있습니다.

예를 들어, 블로그 사이트에서 모든 포스트 페이지는 같은 헤더와 푸터를 가지지만, 관리자 페이지는 다른 사이드바와 네비게이션을 가지는 경우가 그렇습니다. 기존 Vue 앱에서는 App.vue에서 조건부 렌더링으로 레이아웃을 변경하거나, 각 페이지 컴포넌트에서 레이아웃 컴포넌트를 import하여 감싸야 했다면, Nuxt3에서는 파일 시스템 기반으로 자동화됩니다.

Layouts는 <slot /> 또는 <NuxtPage />로 페이지 콘텐츠를 삽입하고, 페이지는 definePageMeta로 사용할 레이아웃을 지정하며, 동적으로 레이아웃을 변경할 수 있고, 중첩 라우팅과 자연스럽게 통합됩니다. 이러한 특징들이 코드를 깔끔하게 만들고 디자인 시스템을 일관되게 유지하며 개발 생산성을 크게 향상시킵니다.

코드 예제

// layouts/default.vue - 기본 레이아웃
<template>
  <div class="app-layout">
    <header>
      <nav>
        <NuxtLink to="/"></NuxtLink>
        <NuxtLink to="/about">소개</NuxtLink>
      </nav>
    </header>

    <main>
      <!-- 페이지 콘텐츠가 여기에 삽입됨 -->
      <slot />
    </main>

    <footer>
      <p>&copy; 2025 My App</p>
    </footer>
  </div>
</template>

// pages/index.vue - 기본 레이아웃 사용
<script setup>
// 레이아웃 지정 (기본값이므로 생략 가능)
definePageMeta({
  layout: 'default'
})
</script>

<template>
  <div>
    <h1>홈 페이지</h1>
    <p>메인 콘텐츠</p>
  </div>
</template>

설명

이것이 하는 일: Layouts 시스템은 재사용 가능한 UI 템플릿을 제공하고, Pages는 그 안에 콘텐츠를 삽입하여, 애플리케이션 전체에서 일관된 디자인을 유지하면서도 페이지마다 다른 레이아웃을 유연하게 사용할 수 있게 합니다. 첫 번째로, layouts/ 디렉토리에 레이아웃 컴포넌트를 만들면 자동으로 등록됩니다.

layouts/default.vue는 기본 레이아웃이 되며, layouts/admin.vue, layouts/auth.vue 같은 다른 레이아웃도 만들 수 있습니다. 레이아웃 내부의 <slot />이 페이지 콘텐츠로 교체되는 방식으로 작동합니다.

이렇게 하는 이유는 Vue의 슬롯 시스템을 활용하여 매우 유연한 구조를 만들기 위함입니다. 그 다음으로, 페이지 컴포넌트에서 definePageMeta({ layout: 'admin' })로 사용할 레이아웃을 지정합니다.

이는 컴파일 타임에 처리되어 런타임 오버헤드가 없으며, TypeScript 타입 체킹도 완벽하게 작동합니다. 레이아웃을 지정하지 않으면 자동으로 default 레이아웃이 적용됩니다.

마지막으로, setPageLayout() composable을 사용하면 런타임에 동적으로 레이아웃을 변경할 수 있습니다. 예를 들어 로그인 상태에 따라 다른 레이아웃을 적용하거나, 사용자의 권한에 따라 레이아웃을 전환하는 등의 고급 기능을 구현할 수 있습니다.

레이아웃 간 전환 시 자동으로 트랜지션 애니메이션도 적용됩니다. 여러분이 이 기능을 사용하면 공통 UI 코드가 한 곳에 집중되어 유지보수가 쉬워지고, 디자인 변경 시 레이아웃 파일만 수정하면 모든 페이지에 자동 반영되며, 서로 다른 레이아웃이 필요한 복잡한 애플리케이션도 깔끔하게 구조화할 수 있습니다.

특히 다양한 사용자 그룹을 대상으로 하는 서비스에서 매우 유용합니다.

실전 팁

💡 레이아웃에서 공통 상태나 로직이 필요하면 useState composable을 사용하세요. 레이아웃이 변경되어도 상태가 유지되어 사용자 경험이 향상됩니다.

💡 에러 페이지에는 레이아웃을 적용하지 않으려면 definePageMeta({ layout: false })로 설정하세요. 에러 상황에서 최소한의 UI만 표시하여 문제를 명확히 할 수 있습니다.

💡 중첩된 레이아웃이 필요하면 부모 레이아웃에서 <NuxtPage />를 사용하세요. 예를 들어 대시보드 안에 여러 서브 섹션이 있을 때 유용합니다.

💡 레이아웃 전환 애니메이션을 커스터마이징하려면 app.vue에서 <NuxtLayout>에 트랜지션을 적용하세요. <NuxtLayout><transition name="fade"><NuxtPage /></transition></NuxtLayout> 형태로 사용합니다.

💡 모바일과 데스크톱에서 다른 레이아웃을 사용하려면 useDevice 같은 composable로 디바이스를 감지하고 setPageLayout으로 동적으로 변경하세요. 반응형 디자인을 넘어 완전히 다른 UI를 제공할 수 있습니다.


6. Server-Side Rendering (SSR) vs Static Generation

시작하며

여러분이 웹사이트를 만들면서 SEO가 중요한지, 데이터가 얼마나 자주 변경되는지, 서버 비용을 얼마나 절약해야 하는지 고민한 적 있나요? SPA로 만들면 개발은 쉽지만 검색 엔진이 콘텐츠를 제대로 인덱싱하지 못하고, 전통적인 SSR은 SEO는 좋지만 서버 부하가 크고 비용이 많이 들고, 정적 사이트는 빠르지만 동적 데이터를 다루기 어려운 상황.

이런 문제는 프로젝트의 성격에 따라 최적의 렌더링 전략이 다르기 때문에 발생합니다. 블로그 같은 콘텐츠 사이트는 정적 생성이 완벽하지만, 실시간 대시보드는 SSR이 필요하고, 마케팅 랜딩 페이지는 두 가지를 혼합해야 할 수도 있습니다.

잘못된 선택은 성능 문제, 높은 서버 비용, 나쁜 SEO로 이어집니다. 바로 이럴 때 필요한 것이 Nuxt3의 유연한 렌더링 모드입니다.

페이지마다 또는 라우트마다 SSR, Static Generation, SPA를 선택적으로 적용하고, 하이브리드 렌더링으로 최적의 성능과 비용 효율을 달성할 수 있습니다.

개요

간단히 말해서, SSR은 요청마다 서버에서 HTML을 생성하고, Static Generation은 빌드 타임에 미리 HTML을 생성하며, Nuxt3는 이 둘을 페이지별로 자유롭게 선택할 수 있게 해줍니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 모든 페이지가 같은 렌더링 전략을 필요로 하지 않습니다.

예를 들어, 블로그 포스트 목록은 하루에 한 번만 업데이트되므로 정적 생성이 완벽하지만, 사용자 대시보드는 실시간 데이터를 보여줘야 하므로 SSR이 필요하고, 로그인 후 개인 설정 페이지는 클라이언트 사이드만으로 충분할 수 있습니다. 전통적으로는 전체 사이트를 하나의 방식으로만 렌더링해야 했습니다.

Next.js나 Gatsby 같은 프레임워크를 사용하더라도 페이지마다 다른 전략을 세밀하게 제어하기 어려웠다면, Nuxt3는 라우트 룰(Route Rules)로 페이지별 렌더링 모드를 완벽하게 제어할 수 있습니다. SSR은 항상 최신 데이터를 보장하고 동적 콘텐츠에 적합하며, Static은 최고의 성능과 낮은 비용을 제공하고, SPA는 풍부한 인터랙션에 적합하고, Hybrid는 이들을 조합하여 최적화합니다.

이러한 특징들이 프로젝트의 요구사항에 맞는 완벽한 아키텍처를 구축할 수 있게 해줍니다.

코드 예제

// nuxt.config.ts - 라우트별 렌더링 전략 설정
export default defineNuxtConfig({
  routeRules: {
    // 정적 생성 - 빌드 타임에 HTML 생성
    '/': { prerender: true },
    '/about': { prerender: true },
    '/blog/**': { prerender: true },

    // SSR - 매 요청마다 서버에서 렌더링
    '/dashboard/**': { ssr: true },
    '/api/**': { ssr: false }, // API 라우트는 SPA 모드

    // ISR (Incremental Static Regeneration) - 주기적 재생성
    '/products/**': {
      swr: 3600, // 1시간마다 재생성
      prerender: true
    },

    // SPA - 클라이언트 사이드만
    '/admin/**': { ssr: false }
  },

  // 기본값은 SSR
  ssr: true
})

설명

이것이 하는 일: Nuxt3의 렌더링 시스템은 각 라우트에 대해 최적의 렌더링 전략을 선택하고, 빌드 타임과 런타임에 적절히 HTML을 생성하여 SEO, 성능, 서버 비용을 모두 최적화합니다. 첫 번째로, routeRules에서 경로 패턴별로 렌더링 모드를 지정합니다.

prerender: true는 빌드 시 해당 페이지를 미리 생성하여 CDN에서 정적 파일로 제공할 수 있게 하고, ssr: true는 매 요청마다 서버에서 최신 데이터로 HTML을 생성하며, ssr: false는 순수 SPA 모드로 클라이언트에서만 렌더링합니다. 이렇게 하는 이유는 각 페이지의 특성에 맞는 최적의 방식을 선택하여 전체 애플리케이션의 효율을 극대화하기 위함입니다.

그 다음으로, ISR(Incremental Static Regeneration)을 swr 옵션으로 구현할 수 있습니다. swr: 3600은 stale-while-revalidate 전략으로, 1시간 동안은 캐시된 정적 페이지를 제공하고 백그라운드에서 새로 생성한다는 의미입니다.

이는 정적 생성의 속도와 SSR의 신선한 데이터를 동시에 얻을 수 있는 강력한 방법입니다. 마지막으로, 와일드카드 패턴(/**)으로 전체 섹션에 일괄 적용하거나, 특정 경로만 예외 처리할 수 있습니다.

예를 들어 /blog/**는 모든 블로그 페이지를 의미하며, Nuxt는 빌드 시 pages/blog/ 디렉토리의 모든 파일을 스캔하여 정적 페이지를 생성합니다. 동적 라우트([id].vue)도 generate 옵션으로 미리 생성할 경로를 지정할 수 있습니다.

여러분이 이 기능을 사용하면 블로그는 정적으로 제공하여 초고속 로딩을 달성하고, 대시보드는 SSR로 실시간 데이터를 표시하며, 전체적으로 서버 비용을 30-50% 절감하고, SEO 점수를 최대화할 수 있습니다. 특히 트래픽이 많은 서비스에서 정적 페이지 비율을 높이면 서버 부하가 극적으로 감소합니다.

실전 팁

💡 빌드 시 생성할 동적 라우트는 nitro.prerender.routes에 배열로 명시하세요. 예: ['/blog/1', '/blog/2']. 또는 크롤러를 사용해 자동으로 발견하게 할 수도 있습니다.

💡 데이터가 자주 변하지 않는 페이지는 무조건 정적 생성하세요. CDN에서 제공되어 TTFB(Time To First Byte)가 10ms 이하로 떨어지고, 서버 비용이 거의 0이 됩니다.

💡 하이브리드 렌더링을 사용할 때는 캐시 전략을 함께 고려하세요. cache-control 헤더를 적절히 설정하면 ISR 효과를 극대화할 수 있습니다.

💡 개발 환경에서는 항상 SSR 모드로 작동하므로, 정적 생성 결과를 확인하려면 pnpm build && pnpm preview로 프로덕션 빌드를 테스트하세요.

💡 페이지 로드 속도를 분석하려면 Lighthouse나 WebPageTest를 사용하세요. 정적 페이지는 보통 90점 이상, SSR은 70-80점 정도의 성능 점수가 나옵니다.


7. Composables로 로직 재사용

시작하며

여러분이 여러 컴포넌트에서 동일한 로직을 반복해서 작성한 적 있나요? 사용자 인증 상태 확인, 데이터 페칭, 폼 유효성 검사, 로컬 스토리지 관리 같은 코드를 복사-붙여넣기하고, 나중에 버그를 수정할 때 모든 곳을 찾아다니며 고쳐야 하는 상황.

이런 문제는 코드 중복을 심화시키고 버그 발생 가능성을 높이며 유지보수를 어렵게 만듭니다. 특히 비즈니스 로직이 컴포넌트 코드와 섞여 있으면 테스트하기도 어렵고, 로직을 변경할 때 UI까지 함께 수정해야 하는 경우가 많습니다.

Options API 시대의 mixins는 이름 충돌과 출처 불명확성 문제가 있었습니다. 바로 이럴 때 필요한 것이 Nuxt3의 Composables입니다.

Vue 3 Composition API를 활용하여 재사용 가능한 로직을 함수로 추출하고, 자동 임포트로 어디서든 쉽게 사용하며, 타입 안정성과 테스트 용이성을 동시에 확보할 수 있습니다.

개요

간단히 말해서, Composables는 Vue의 반응형 시스템과 라이프사이클을 활용하는 재사용 가능한 함수로, composables/ 디렉토리에 작성하면 자동으로 임포트됩니다. 왜 이 기능이 필요한지 실무 관점에서 설명하면, 실제 애플리케이션은 많은 공통 로직을 가집니다.

예를 들어, 여러 페이지에서 현재 로그인한 사용자 정보를 표시하거나, 다크 모드 설정을 읽고 적용하거나, 폼 입력을 검증하는 등의 작업은 반복적으로 발생합니다. 기존에는 이런 로직을 각 컴포넌트에 개별적으로 작성하거나, Vuex 같은 전역 상태 관리 라이브러리에 의존해야 했다면, Composables는 더 가볍고 유연한 방식으로 로직을 공유할 수 있게 해줍니다.

Composables는 반응형 상태와 로직을 캡슐화하고, 여러 컴포넌트에서 독립적으로 사용 가능하며, TypeScript 타입 추론이 완벽하게 작동하고, 유닛 테스트하기 매우 쉽습니다. 이러한 특징들이 코드 재사용성을 극대화하고 관심사의 분리를 명확히 하며 개발 효율성을 크게 향상시킵니다.

코드 예제

// composables/useAuth.ts - 인증 로직 재사용
export const useAuth = () => {
  // 전역 상태 - 모든 곳에서 같은 인스턴스 공유
  const user = useState<User | null>('user', () => null)
  const isAuthenticated = computed(() => user.value !== null)

  // 로그인 함수
  const login = async (email: string, password: string) => {
    try {
      const data = await $fetch('/api/auth/login', {
        method: 'POST',
        body: { email, password }
      })
      user.value = data.user
      return { success: true }
    } catch (error) {
      return { success: false, error: error.message }
    }
  }

  // 로그아웃 함수
  const logout = async () => {
    await $fetch('/api/auth/logout', { method: 'POST' })
    user.value = null
  }

  // 초기화 - 세션 복원
  const init = async () => {
    try {
      const data = await $fetch('/api/auth/session')
      user.value = data.user
    } catch {
      user.value = null
    }
  }

  return {
    user: readonly(user),
    isAuthenticated,
    login,
    logout,
    init
  }
}

설명

이것이 하는 일: Composables는 Vue의 Composition API를 활용하여 상태 관리, 사이드 이펙트, 계산된 값 등의 로직을 독립적인 함수로 추출하고, 이를 컴포넌트에서 조합하여 사용할 수 있게 해줍니다. 첫 번째로, use로 시작하는 네이밍 컨벤션을 따르는 함수를 만들고 내부에서 Vue의 반응형 API를 사용합니다.

useState는 Nuxt가 제공하는 특별한 composable로, 서버와 클라이언트 간에 상태를 자동으로 공유하고, 같은 키를 사용하면 전역 싱글톤처럼 동작합니다. 이렇게 하는 이유는 SSR 환경에서도 안전하게 상태를 관리하고, 불필요한 전역 상태 라이브러리 없이도 데이터를 공유하기 위함입니다.

그 다음으로, composable 내부에서 비즈니스 로직을 함수로 정의합니다. login, logout 같은 함수들은 API를 호출하고 상태를 업데이트하는 작업을 캡슐화합니다.

각 컴포넌트에서는 이 composable을 호출하기만 하면 되므로, 인증 로직의 구현 세부사항을 알 필요가 없습니다. readonly로 감싸면 외부에서 직접 수정하지 못하게 보호할 수 있습니다.

마지막으로, 반환된 값들은 모두 반응형으로 작동합니다. user 값이 변경되면 이를 사용하는 모든 컴포넌트의 UI가 자동으로 업데이트되고, isAuthenticated 같은 computed 값도 자동으로 재계산됩니다.

이는 Vue의 반응형 시스템 덕분이며, 개발자가 수동으로 업데이트를 트리거할 필요가 없습니다. 여러분이 이 기능을 사용하면 인증 로직을 한 곳에서 관리하여 버그를 줄이고, 새로운 컴포넌트에서도 const { user, login } = useAuth()로 즉시 사용할 수 있으며, 테스트 시 composable만 독립적으로 테스트하면 되고, TypeScript로 완벽한 타입 안정성을 확보할 수 있습니다.

특히 복잡한 상태 로직을 컴포넌트에서 분리하면 코드 가독성이 크게 향상됩니다.

실전 팁

💡 여러 composable을 조합하여 더 복잡한 로직을 만들 수 있습니다. 예: useAuth + usePermissions로 권한 기반 접근 제어를 구현하세요.

💡 SSR 환경에서 브라우저 전용 API(localStorage, window 등)를 사용할 때는 onMounted 훅 안에서 호출하거나 process.client 체크를 추가하세요. 서버에서 실행되면 에러가 발생합니다.

💡 비동기 초기화가 필요한 composable은 callOnce나 플러그인에서 호출하세요. 예를 들어 앱 시작 시 한 번만 세션을 복원해야 한다면 플러그인이 적합합니다.

💡 composable은 <script setup> 또는 setup() 함수 내부에서만 호출해야 합니다. 일반 함수나 비동기 콜백 안에서 호출하면 반응형 컨텍스트가 유실될 수 있습니다.

💡 디버깅을 위해 Vue DevTools를 사용하세요. composable이 생성한 ref와 computed를 실시간으로 확인하고 값을 수정해볼 수 있어 문제 해결이 쉬워집니다.


8. Nuxt Config와 환경 설정

시작하며

여러분이 프로젝트를 진행하면서 개발, 스테이징, 프로덕션 환경마다 다른 API URL을 사용하거나, 빌드 최적화 옵션을 조정하거나, 써드파티 라이브러리를 설정하거나, 환경 변수를 안전하게 관리해야 했던 경험이 있나요? 설정 파일이 여기저기 흩어져 있고, 환경별로 다른 값을 하드코딩하거나, 보안에 민감한 API 키를 코드에 직접 넣는 위험한 상황.

이런 문제는 배포 과정을 복잡하게 만들고 보안 취약점을 만들 수 있습니다. 특히 팀 협업 시 각자 다른 로컬 설정을 사용하거나, 프로덕션과 개발 환경의 차이로 인한 버그가 발생하기도 합니다.

또한 성능 최적화, SEO 설정, 보안 헤더 등 복잡한 설정들을 일일이 찾아서 적용하기도 어렵습니다. 바로 이럴 때 필요한 것이 Nuxt3의 통합 설정 시스템입니다.

nuxt.config.ts 하나로 모든 설정을 중앙에서 관리하고, runtimeConfig로 환경별 변수를 안전하게 다루며, TypeScript 타입 지원으로 설정 오류를 컴파일 타임에 잡을 수 있습니다.

개요

간단히 말해서, nuxt.config.ts는 Nuxt 애플리케이션의 모든 설정을 정의하는 중앙 파일이며, runtimeConfig는 환경별로 다른 값을 안전하게 관리하는 시스템입니다. 왜 이 기능이 필요한지 실무 관점에서 설명하면, 모던 웹 애플리케이션은 수많은 설정이 필요합니다.

예를 들어, API 엔드포인트 URL, 인증 토큰, CDN 경로, 빌드 최적화 옵션, SEO 메타 태그, 보안 헤더, 써드파티 서비스 키 등을 개발과 프로덕션 환경에서 다르게 사용해야 합니다. 기존에는 .env 파일, webpack 설정, package.json 스크립트 등 여러 곳에 설정이 분산되어 있고, 환경 변수를 클라이언트에서 안전하게 사용하기 어려웠다면, Nuxt3는 모든 것을 하나로 통합하고 타입 안전성까지 제공합니다.

Nuxt Config는 애플리케이션의 모든 측면을 제어하고, runtimeConfig는 서버 전용과 공개 변수를 구분하며, 환경 변수를 자동으로 주입하고, TypeScript 자동 완성과 검증을 제공합니다. 이러한 특징들이 설정 관리를 단순화하고 보안을 강화하며 개발 생산성을 높여줍니다.

코드 예제

// nuxt.config.ts - 통합 설정 파일
export default defineNuxtConfig({
  // 런타임 설정 - 환경 변수 관리
  runtimeConfig: {
    // 서버 전용 (비공개) - 클라이언트에 노출되지 않음
    apiSecret: process.env.API_SECRET,
    dbUrl: process.env.DATABASE_URL,

    // 공개 (클라이언트에서 접근 가능)
    public: {
      apiBase: process.env.NUXT_PUBLIC_API_BASE || 'http://localhost:3000',
      appName: 'My Nuxt App'
    }
  },

  // 앱 설정
  app: {
    head: {
      title: 'Nuxt3 App',
      meta: [
        { charset: 'utf-8' },
        { name: 'viewport', content: 'width=device-width, initial-scale=1' }
      ]
    }
  },

  // 빌드 최적화
  nitro: {
    compressPublicAssets: true,
    prerender: {
      crawlLinks: true,
      routes: ['/sitemap.xml']
    }
  },

  // TypeScript 설정
  typescript: {
    strict: true,
    typeCheck: true
  }
})

설명

이것이 하는 일: Nuxt Config는 애플리케이션의 빌드, 런타임, SEO, 성능 최적화 등 모든 설정을 한 곳에서 관리하고, 환경별로 다른 값을 자동으로 주입하며, 타입 체킹으로 설정 오류를 사전에 방지합니다. 첫 번째로, runtimeConfig는 서버 전용 변수와 공개 변수를 명확히 구분합니다.

최상위 레벨의 속성(apiSecret, dbUrl)은 서버에서만 접근 가능하고 클라이언트 번들에 포함되지 않아 보안이 유지됩니다. public 아래의 속성은 클라이언트에서도 접근할 수 있으며, 컴파일 타임이 아닌 런타임에 주입되어 Docker 이미지를 재빌드하지 않고도 환경 변수를 변경할 수 있습니다.

이렇게 하는 이유는 보안과 유연성을 동시에 확보하기 위함입니다. 그 다음으로, 환경 변수는 자동으로 매핑됩니다.

.env 파일에 NUXT_PUBLIC_API_BASE=https://api.example.com로 정의하면 자동으로 runtimeConfig.public.apiBase에 주입됩니다. 네이밍 컨벤션은 NUXT_으로 시작하고 중첩된 속성은 언더스코어로 연결합니다.

코드에서는 useRuntimeConfig()로 접근하며, 서버와 클라이언트 모두에서 동일한 방식으로 사용할 수 있습니다. 마지막으로, 다른 설정들도 모두 타입 체킹됩니다.

app.head로 SEO 메타 태그를 정의하고, nitro로 서버 최적화를 설정하고, typescript로 엄격한 타입 체킹을 활성화할 수 있습니다. IDE에서 자동 완성이 제공되므로 설정 키를 잘못 입력하거나 타입을 틀리게 지정하는 실수를 방지할 수 있습니다.

여러분이 이 기능을 사용하면 모든 설정이 한 파일에 집중되어 관리가 쉬워지고, 민감한 정보가 클라이언트에 노출되지 않아 보안이 강화되며, 환경별 배포가 간단해지고, 팀원 간 설정 공유가 명확해집니다. 특히 CI/CD 파이프라인에서 환경 변수만 바꿔서 동일한 빌드를 여러 환경에 배포할 수 있어 DevOps가 크게 개선됩니다.

실전 팁

💡 민감한 정보는 절대 public에 넣지 마세요. API 키, 데이터베이스 비밀번호 등은 반드시 최상위 레벨에 두어 서버 전용으로 관리하세요.

💡 개발 환경에서는 .env 파일을 사용하고, 프로덕션에서는 시스템 환경 변수를 사용하세요. .env를 git에 커밋하지 말고 .env.example로 템플릿만 공유하세요.

💡 runtimeConfig의 기본값은 fallback으로 작동합니다. 환경 변수가 없을 때 사용되므로, 안전한 기본값을 설정하여 설정 누락으로 인한 에러를 방지하세요.

💡 복잡한 설정은 별도 파일로 분리하고 import하세요. 예: import { seoConfig } from './config/seo'로 가독성을 높일 수 있습니다.

💡 성능 최적화를 위해 nitro.compressPublicAssets: true를 활성화하세요. 정적 파일이 gzip/brotli로 압축되어 전송 크기가 70% 이상 줄어듭니다.


9. Nitro 엔진과 서버 미들웨어

시작하며

여러분이 Node.js 서버를 구축하면서 Express나 Fastify 같은 프레임워크를 설정하고, 라우팅을 수동으로 구성하고, 미들웨어를 체인으로 연결하고, 배포 환경마다 다른 어댑터를 작성해야 했던 경험이 있나요? 프로덕션에서 서버 성능이 기대에 못 미치거나, 서버리스 환경으로 마이그레이션하려니 코드를 전부 다시 작성해야 하는 상황.

이런 문제는 전통적인 Node.js 서버 프레임워크의 한계에서 발생합니다. 특히 SSR과 API를 함께 제공하는 풀스택 애플리케이션에서는 두 가지 서버 로직을 통합하기 어렵고, 다양한 배포 플랫폼(Vercel, Netlify, AWS Lambda, Cloudflare Workers)에 대응하기 위해 각각 다른 설정을 관리해야 합니다.

바로 이럴 때 필요한 것이 Nuxt3의 Nitro 엔진입니다. 차세대 서버 엔진으로 극도로 빠른 성능을 제공하고, 모든 주요 플랫폼에 자동으로 배포 가능하며, API와 SSR을 통합 관리하고, 강력한 캐싱과 최적화를 내장하고 있습니다.

개요

간단히 말해서, Nitro는 Nuxt3의 서버 엔진으로, 매우 빠르고 가벼우며 어디든 배포 가능한 범용 서버 프레임워크입니다. 왜 이 기능이 필요한지 실무 관점에서 설명하면, 현대 웹 애플리케이션은 다양한 배포 옵션이 필요합니다.

예를 들어, 개발 중에는 로컬 Node.js 서버로 실행하고, 스테이징은 Vercel에, 프로덕션은 AWS Lambda에 배포하고 싶을 수 있습니다. 기존에는 각 플랫폼마다 다른 설정과 코드가 필요했습니다.

전통적인 Express 서버는 플랫폼 종속적이고 무겁고 수동 설정이 많았다면, Nitro는 빌드 타임에 타겟 플랫폼을 선택하면 자동으로 최적화된 코드를 생성합니다. Nitro는 H3 프레임워크 기반으로 Express보다 2-3배 빠르고, 자동 코드 스플리팅으로 번들 크기를 최소화하며, 스토리지 레이어로 파일/Redis/Cloudflare KV 등을 통합 지원하고, 15개 이상의 배포 프리셋을 제공합니다.

이러한 특징들이 서버 성능을 극대화하고 배포 유연성을 제공하며 개발자 경험을 크게 향상시킵니다.

코드 예제

// server/middleware/logger.ts - 서버 미들웨어
export default defineEventHandler((event) => {
  // 모든 요청에 대해 실행됨
  const start = Date.now()
  const path = event.node.req.url

  // 요청 전 처리
  console.log(`[${new Date().toISOString()}] → ${path}`)

  // 응답 후 처리 (onAfterResponse 훅)
  event.node.res.on('finish', () => {
    const duration = Date.now() - start
    const status = event.node.res.statusCode
    console.log(`[${new Date().toISOString()}] ← ${path} ${status} (${duration}ms)`)
  })
})

// server/api/cached.get.ts - 캐싱 예제
export default defineCachedEventHandler(
  async (event) => {
    // 무거운 데이터베이스 쿼리
    const data = await fetchExpensiveData()
    return data
  },
  {
    maxAge: 60 * 10, // 10분 캐싱
    getKey: (event) => event.node.req.url // 캐시 키
  }
)

설명

이것이 하는 일: Nitro 엔진은 서버 사이드 로직을 실행하고, 요청을 라우팅하고, 미들웨어를 처리하며, 빌드 타임에 타겟 플랫폼에 최적화된 번들을 생성하여 어디서나 빠르게 실행될 수 있게 합니다. 첫 번째로, server/middleware/ 디렉토리의 파일들은 모든 요청에 대해 자동으로 실행됩니다.

위 코드에서 로거 미들웨어는 요청 시작 시간을 기록하고, 응답이 완료되면 소요 시간을 로깅합니다. 이는 Express의 app.use()와 유사하지만, Nuxt가 파일 시스템 기반으로 자동 등록한다는 점이 다릅니다.

이렇게 하는 이유는 설정 없이도 미들웨어를 추가할 수 있게 하여 개발 속도를 높이기 위함입니다. 그 다음으로, defineCachedEventHandler는 API 응답을 자동으로 캐싱합니다.

maxAge로 캐시 유지 시간을 지정하고, getKey로 캐시 키를 커스터마이징할 수 있습니다. 내부적으로는 메모리 기반 캐시를 사용하지만, nitro.storage를 설정하면 Redis, Cloudflare KV, Vercel KV 등 외부 스토리지를 사용할 수도 있습니다.

데이터베이스 부하를 크게 줄이고 응답 속도를 10배 이상 향상시킬 수 있습니다. 마지막으로, Nitro는 빌드 시 타겟 플랫폼을 감지하고 최적화된 출력을 생성합니다.

nuxt build를 실행하면 .output/ 디렉토리에 배포 가능한 파일들이 생성되며, Vercel이나 Netlify에 배포하면 자동으로 엣지 함수로 변환되고, Node.js 서버로 배포하면 standalone 앱으로 번들링됩니다. 코드를 전혀 수정하지 않고도 다양한 인프라에 배포할 수 있습니다.

여러분이 이 기능을 사용하면 서버 응답 시간이 30-50% 단축되고, 데이터베이스 부하가 캐싱으로 크게 감소하며, 배포 플랫폼을 자유롭게 선택할 수 있고, 미들웨어로 로깅, 인증, CORS 등을 중앙에서 관리할 수 있습니다. 특히 서버리스 환경에서도 최적의 성능을 발휘하여 Cold Start 시간이 최소화됩니다.

실전 팁

💡 미들웨어 실행 순서를 제어하려면 파일명 앞에 숫자를 붙이세요. 예: 01.cors.ts, 02.auth.ts는 순서대로 실행됩니다.

💡 글로벌 에러 핸들링을 위해 server/middleware/error.ts에서 try-catch로 모든 요청을 감싸세요. 일관된 에러 응답 형식을 제공할 수 있습니다.

💡 Redis 캐싱을 사용하려면 nitro.storage에 redis 드라이버를 설정하세요. 여러 서버 인스턴스 간 캐시를 공유하여 확장성이 향상됩니다.

💡 API 요청 속도 제한(Rate Limiting)은 @nuxt/security 모듈을 사용하거나, 미들웨어에서 직접 구현할 수 있습니다. IP별 요청 횟수를 캐시에 저장하여 체크하세요.

💡 프로덕션 배포 시 NITRO_PRESET 환경 변수로 타겟을 명시할 수 있습니다. 예: NITRO_PRESET=cloudflare pnpm build로 Cloudflare Workers용 빌드를 생성합니다.


10. 프로덕션 배포와 최적화

시작하며

여러분이 개발한 애플리케이션을 실제 사용자에게 서비스하려고 할 때, 빌드 시간이 너무 오래 걸리거나, 번들 크기가 커서 초기 로딩이 느리거나, SEO 점수가 낮게 나오거나, 서버 비용이 예상보다 많이 나오는 경험을 한 적 있나요? 개발 환경에서는 잘 작동하는데 프로덕션에서는 성능이 떨어지고, 어디를 최적화해야 할지 막막한 상황.

이런 문제는 프로덕션 배포 전에 충분한 최적화를 하지 않아서 발생합니다. 특히 이미지 최적화, 코드 스플리팅, 트리 쉐이킹, 캐싱 전략, CDN 설정 등 신경 써야 할 것이 많고, 하나라도 놓치면 사용자 경험이 크게 저하됩니다.

Lighthouse 점수가 낮으면 SEO 순위도 떨어지고 사용자 이탈률이 높아집니다. 바로 이럴 때 필요한 것이 체계적인 프로덕션 최적화 전략입니다.

Nuxt3는 많은 최적화를 자동으로 제공하지만, 추가로 적용할 수 있는 베스트 프랙티스들을 알고 실행하면 성능을 2-3배 이상 향상시킬 수 있습니다.

개요

간단히 말해서, 프로덕션 배포는 애플리케이션을 실제 사용자에게 제공하기 위해 최적화하고 빌드하고 인프라에 올리는 전체 과정입니다. 왜 이 과정이 필요한지 실무 관점에서 설명하면, 개발 모드와 프로덕션은 완전히 다른 요구사항을 가집니다.

예를 들어, 개발에서는 핫 리로딩과 디버깅이 중요하지만, 프로덕션에서는 번들 크기 최소화, 빠른 로딩 속도, 높은 SEO 점수, 안정성, 보안이 핵심입니다. 기존에는 webpack 설정을 수동으로 튜닝하고, 이미지를 일일이 최적화하고, 캐싱 헤더를 수동으로 설정하고, 각 플랫폼마다 다른 배포 스크립트를 작성해야 했다면, Nuxt3는 대부분을 자동화하고 나머지도 간단한 설정으로 해결합니다.

프로덕션 최적화는 번들 크기 감소로 로딩 속도 향상, 트리 쉐이킹으로 사용하지 않는 코드 제거, 코드 스플리팅으로 초기 로드 최소화, 이미지 최적화로 대역폭 절감을 달성합니다. 이러한 기법들이 Lighthouse 점수를 90점 이상으로 만들고 사용자 만족도를 크게 높입니다.

코드 예제

// nuxt.config.ts - 프로덕션 최적화 설정
export default defineNuxtConfig({
  // 프로덕션 최적화
  nitro: {
    compressPublicAssets: true, // 정적 파일 압축
    minify: true, // 코드 최소화

    // 사전 렌더링으로 정적 페이지 생성
    prerender: {
      crawlLinks: true,
      routes: ['/sitemap.xml', '/robots.txt']
    }
  },

  // 실험적 기능으로 성능 향상
  experimental: {
    payloadExtraction: true, // 페이로드 추출로 번들 크기 감소
    inlineSSRStyles: false, // CSS 인라인 방지
    renderJsonPayloads: true // JSON 페이로드 최적화
  },

  // 빌드 최적화
  vite: {
    build: {
      rollupOptions: {
        output: {
          manualChunks: {
            // 큰 라이브러리를 별도 청크로 분리
            'vendor': ['vue', 'vue-router']
          }
        }
      }
    }
  },

  // 이미지 최적화 (nuxt/image 모듈 사용)
  image: {
    formats: ['webp', 'avif'],
    quality: 80,
    screens: {
      xs: 320,
      sm: 640,
      md: 768,
      lg: 1024,
      xl: 1280
    }
  }
})

// package.json - 배포 스크립트
{
  "scripts": {
    "build": "nuxt build",
    "preview": "nuxt preview",
    "deploy": "pnpm build && pm2 restart nuxt-app"
  }
}

설명

이것이 하는 일: 프로덕션 최적화는 빌드 타임과 런타임에서 애플리케이션의 성능을 최대화하고, 불필요한 코드와 리소스를 제거하며, 로딩 속도를 극대화하여 사용자에게 최고의 경험을 제공합니다. 첫 번째로, Nitro 설정으로 서버 측 최적화를 적용합니다.

compressPublicAssets는 모든 정적 파일(JS, CSS, HTML)을 gzip/brotli로 압축하여 전송 크기를 70% 이상 줄이고, minify는 코드를 최소화하여 공백과 주석을 제거하며, prerender는 빌드 시 정적 페이지를 미리 생성하여 서버 부하를 줄입니다. 이렇게 하는 이유는 네트워크 전송량을 최소화하고 서버 응답 시간을 단축하기 위함입니다.

그 다음으로, experimental 옵션으로 최신 최적화 기법을 활성화합니다. payloadExtraction은 SSR 페이로드를 HTML에서 분리하여 캐싱 효율을 높이고, renderJsonPayloads는 데이터를 효율적으로 직렬화합니다.

이러한 실험적 기능들은 향후 정식 기능이 될 예정이며, 이미 프로덕션에서 안정적으로 사용할 수 있습니다. 마지막으로, Vite 빌드 설정으로 코드 스플리팅을 세밀하게 제어합니다.

manualChunks로 큰 라이브러리를 별도 청크로 분리하면 브라우저 캐싱 효율이 높아지고, 메인 번들 크기가 줄어들어 초기 로딩이 빨라집니다. 예를 들어 Vue 런타임은 거의 변경되지 않으므로 별도 청크로 분리하면 사용자가 한 번만 다운로드하고 계속 캐시에서 사용할 수 있습니다.

여러분이 이러한 최적화를 적용하면 Lighthouse 성능 점수가 60-70점대에서 90점 이상으로 향상되고, 초기 로딩 시간이 50% 이상 단축되며, 서버 비용이 감소하고, SEO 순위가 상승하여 자연 유입 트래픽이 증가합니다. 특히 모바일 사용자 경험이 크게 개선되어 이탈률이 낮아집니다.

실전 팁

💡 이미지는 반드시 @nuxt/image 모듈을 사용하여 최적화하세요. 자동으로 WebP/AVIF 변환, 반응형 크기 생성, lazy loading을 제공하여 페이지 속도가 크게 향상됩니다.

💡 번들 분석기로 큰 의존성을 찾으세요. nuxt analyze 명령으로 번들 크기를 시각화하고, 불필요한 라이브러리를 제거하거나 더 가벼운 대안으로 교체하세요.

💡 CDN을 사용하여 정적 파일을 제공하세요. Cloudflare, AWS CloudFront, Vercel Edge Network 등을 활용하면 전 세계 사용자에게 밀리초 단위로 콘텐츠를 전달할 수 있습니다.

💡 크리티컬 CSS를 인라인하고 나머지는 비동기 로드하세요. @nuxtjs/critters 모듈을 사용하면 자동으로 처리되어 First Contentful Paint가 빨라집니다.

💡 프로덕션 배포 전에 반드시 pnpm build && pnpm preview로 로컬에서 테스트하세요. 개발 모드와 다른 문제를 미리 발견할 수 있습니다.


#Nuxt3#ServerRoutes#Composables#DataFetching#SSR#Vue