본문으로 건너뛰기
Advertisement

15.2 환경 설정 — nuxi init, 디렉토리 구조, nuxt.config.ts

프로젝트 생성

Nuxt 3 프로젝트는 공식 CLI 도구인 nuxi를 사용하여 생성합니다.

기본 프로젝트 생성

# 최신 nuxi로 프로젝트 생성
npx nuxi@latest init my-nuxt-app

# 생성 후 해당 디렉토리로 이동
cd my-nuxt-app

# 의존성 설치
npm install

# 개발 서버 실행
npm run dev

개발 서버가 실행되면 http://localhost:3000에서 앱을 확인할 수 있습니다.

템플릿 선택

nuxi 초기화 시 몇 가지 질문을 통해 프로젝트를 설정합니다.

? Which package manager would you like to use?
❯ npm
pnpm
yarn
bun

? Initialize git repository?
❯ Yes
No

추가 모듈과 함께 생성

# TypeScript + ESLint + Tailwind CSS 포함
npx nuxi@latest init my-app --template github:nuxt/starter#v3

# 또는 Nuxt UI 스타터
npx nuxi@latest init my-app -t ui

디렉토리 구조

Nuxt 3 프로젝트의 전체 디렉토리 구조를 이해하는 것이 중요합니다. 각 디렉토리는 특별한 역할을 가집니다.

전체 구조 개요

my-nuxt-app/
├── .nuxt/ # 빌드 캐시 (자동 생성, git ignore)
├── .output/ # 프로덕션 빌드 결과물
├── assets/ # 빌드 도구가 처리하는 정적 파일
│ ├── css/
│ │ └── main.css
│ └── images/
├── components/ # Vue 컴포넌트 (자동 임포트)
│ ├── AppHeader.vue
│ ├── AppFooter.vue
│ └── ui/
│ ├── Button.vue
│ └── Card.vue
├── composables/ # 컴포저블 함수 (자동 임포트)
│ ├── useAuth.ts
│ └── useApi.ts
├── layouts/ # 레이아웃 컴포넌트
│ ├── default.vue
│ └── admin.vue
├── middleware/ # 라우트 미들웨어
│ ├── auth.ts
│ └── logger.ts
├── pages/ # 파일 기반 라우팅
│ ├── index.vue
│ ├── about.vue
│ └── users/
│ ├── index.vue
│ └── [id].vue
├── plugins/ # Nuxt 플러그인
│ ├── analytics.client.ts # 클라이언트 전용
│ └── auth.server.ts # 서버 전용
├── public/ # 빌드 도구 처리 없는 정적 파일
│ ├── favicon.ico
│ └── robots.txt
├── server/ # 서버 사이드 코드
│ ├── api/ # API 라우트
│ │ └── users/
│ │ ├── index.get.ts
│ │ └── [id].get.ts
│ ├── middleware/ # 서버 미들웨어
│ │ └── logger.ts
│ └── utils/ # 서버 유틸리티 (자동 임포트)
│ └── db.ts
├── utils/ # 클라이언트/서버 공통 유틸리티 (자동 임포트)
│ └── format.ts
├── app.vue # 앱 루트 컴포넌트
├── nuxt.config.ts # Nuxt 설정
├── tsconfig.json # TypeScript 설정
└── package.json

핵심 디렉토리 설명

pages/ — 파일 기반 라우팅

pages/
├── index.vue → /
├── about.vue → /about
├── blog/
│ ├── index.vue → /blog
│ └── [slug].vue → /blog/:slug
└── admin/
├── index.vue → /admin
└── users/
├── index.vue → /admin/users
└── [id]/
├── index.vue → /admin/users/:id
└── edit.vue → /admin/users/:id/edit

components/ — 자동 임포트 컴포넌트

components/
├── Button.vue → <Button />
├── TheHeader.vue → <TheHeader /> (전역 단일 컴포넌트 관례)
└── form/
├── Input.vue → <FormInput />
└── Select.vue → <FormSelect />

폴더 구조가 컴포넌트 이름의 접두사가 됩니다.

server/api/ — API 라우트

파일명 규칙: [경로].[메서드].ts

server/api/
├── users/
│ ├── index.get.ts → GET /api/users
│ ├── index.post.ts → POST /api/users
│ └── [id].get.ts → GET /api/users/:id
└── auth/
├── login.post.ts → POST /api/auth/login
└── logout.post.ts → POST /api/auth/logout

assets/ vs public/

구분assets/public/
처리 방식Vite/Webpack이 번들링그대로 복사
URL해시 포함 (/img.abc123.png)그대로 (/img.png)
임포트~/assets/img.png/img.png
용도CSS, 이미지, 폰트favicon, robots.txt

nuxt.config.ts 완전 가이드

nuxt.config.ts는 Nuxt 애플리케이션의 모든 설정을 담당합니다.

기본 구조

// nuxt.config.ts
export default defineNuxtConfig({
// 개발 도구 활성화 (자동으로 개발 모드에서만 동작)
devtools: { enabled: true },
})

완전한 설정 예제

// nuxt.config.ts
export default defineNuxtConfig({
// ─── 개발 도구 ───
devtools: { enabled: true },

// ─── TypeScript ───
typescript: {
strict: true, // 엄격한 타입 검사
typeCheck: true, // 빌드 시 타입 체크
},

// ─── 모듈 ───
modules: [
'@nuxtjs/tailwindcss',
'@pinia/nuxt',
'@nuxtjs/i18n',
'@vueuse/nuxt',
'@nuxt/image',
],

// ─── CSS ───
css: ['~/assets/css/main.css'],

// ─── 런타임 설정 (환경 변수) ───
runtimeConfig: {
// 서버 전용 (클라이언트에서 접근 불가)
dbPassword: process.env.DB_PASSWORD,
jwtSecret: process.env.JWT_SECRET,

// public: 클라이언트와 서버 모두 접근 가능
public: {
apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL || 'http://localhost:3000',
appName: '내 Nuxt 앱',
}
},

// ─── 앱 설정 ───
app: {
head: {
charset: 'utf-8',
viewport: 'width=device-width, initial-scale=1',
title: '내 Nuxt 앱',
meta: [
{ name: 'description', content: 'Nuxt 3로 만든 풀스택 앱입니다.' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
],
},
// 페이지 전환 애니메이션
pageTransition: { name: 'page', mode: 'out-in' },
layoutTransition: { name: 'layout', mode: 'out-in' },
},

// ─── 라우터 설정 ───
router: {
options: {
// 해시 기반 라우팅 (SPA 배포 시 유용)
// hashMode: true,
}
},

// ─── 빌드 최적화 ───
build: {
// 트랜스파일 목록
transpile: ['@vue/apollo-composable'],
},

// ─── Vite 설정 ───
vite: {
css: {
preprocessorOptions: {
scss: {
additionalData: '@use "~/assets/scss/variables" as *;',
},
},
},
// 개발 서버 프록시
server: {
proxy: {
'/external-api': {
target: 'https://api.external.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/external-api/, ''),
}
}
}
},

// ─── Nitro (서버 엔진) ───
nitro: {
preset: 'node-server',
// 서버 스토리지
storage: {
redis: {
driver: 'redis',
host: process.env.REDIS_HOST || 'localhost',
port: 6379,
}
},
// 라우트 규칙
routeRules: {
'/': { prerender: true }, // 정적으로 사전 렌더링
'/blog/**': { swr: 3600 }, // 1시간 캐시 (ISR)
'/api/**': { cors: true }, // API CORS 허용
'/admin/**': { ssr: false }, // CSR 전환
}
},

// ─── 실험적 기능 ───
experimental: {
payloadExtraction: false, // 정적 배포 최적화
typedPages: true, // 타입 안전한 라우팅
},
})

routeRules 활용

routeRules는 Nuxt 3의 강력한 기능으로, 페이지별로 렌더링 전략을 다르게 설정할 수 있습니다.

nitro: {
routeRules: {
// 홈페이지: 사전 렌더링 (정적)
'/': { prerender: true },

// 블로그 목록: 1시간마다 갱신 (SWR = Stale While Revalidate)
'/blog': { swr: 3600 },

// 블로그 포스트: 24시간 캐시 후 갱신
'/blog/**': { swr: 86400 },

// 제품 페이지: CDN 캐시 1일
'/products/**': { cache: { maxAge: 60 * 60 * 24 } },

// 관리자 페이지: SSR 비활성화 (CSR)
'/admin/**': { ssr: false },

// API: CORS 허용, 캐시 없음
'/api/**': { cors: true, headers: { 'cache-control': 'no-cache' } },

// 레거시 URL 리다이렉트
'/old-page': { redirect: '/new-page' },
}
}

환경 변수와 런타임 설정

.env 파일 설정

# .env
# 서버 전용 변수
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
JWT_SECRET=my-secret-key-here
REDIS_URL=redis://localhost:6379

# 공개 변수 (NUXT_PUBLIC_ 접두사)
NUXT_PUBLIC_API_BASE_URL=https://api.example.com
NUXT_PUBLIC_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX

runtimeConfig 사용 방법

// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// 서버 전용
databaseUrl: '', // process.env.NUXT_DATABASE_URL로 자동 오버라이드
jwtSecret: '', // process.env.NUXT_JWT_SECRET으로 자동 오버라이드

public: {
// 클라이언트 + 서버
apiBase: '', // process.env.NUXT_PUBLIC_API_BASE로 오버라이드
}
}
})
// server/api/users/index.get.ts
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()

// 서버에서는 비공개 변수 접근 가능
console.log(config.jwtSecret) // 서버 전용
console.log(config.public.apiBase) // 공개 변수
})
<!-- pages/index.vue -->
<script setup lang="ts">
const config = useRuntimeConfig()

// 클라이언트에서는 public만 접근 가능
console.log(config.public.apiBase) // OK
// console.log(config.jwtSecret) // undefined (보안상 접근 불가)
</script>

실전 예제: E-커머스 프로젝트 설정

실제 E-커머스 프로젝트의 완전한 설정 예제입니다.

프로젝트 초기화

# 프로젝트 생성
npx nuxi@latest init my-shop

cd my-shop

# 필요한 모듈 설치
npm install @pinia/nuxt @pinia/nuxt @nuxtjs/tailwindcss
npm install @vueuse/nuxt @nuxt/image
npm install -D @nuxtjs/i18n

# 개발 의존성
npm install -D @nuxt/devtools

완전한 nuxt.config.ts

// nuxt.config.ts
export default defineNuxtConfig({
devtools: { enabled: true },

modules: [
'@nuxtjs/tailwindcss',
'@pinia/nuxt',
'@vueuse/nuxt',
'@nuxt/image',
],

css: [
'~/assets/css/main.css',
],

runtimeConfig: {
// 서버 전용 환경 변수
stripe: {
secretKey: process.env.STRIPE_SECRET_KEY || '',
},
db: {
url: process.env.DATABASE_URL || '',
},
jwt: {
secret: process.env.JWT_SECRET || 'dev-secret',
expiresIn: '7d',
},

public: {
appName: '내 쇼핑몰',
stripe: {
publishableKey: process.env.NUXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || '',
},
features: {
reviews: true,
wishlist: true,
chat: false,
}
}
},

app: {
head: {
charset: 'utf-8',
viewport: 'width=device-width, initial-scale=1',
htmlAttrs: { lang: 'ko' },
titleTemplate: '%s | 내 쇼핑몰',
meta: [
{ name: 'theme-color', content: '#3b82f6' },
{ property: 'og:type', content: 'website' },
],
},
pageTransition: { name: 'fade', mode: 'out-in' },
},

nitro: {
routeRules: {
// 홈, 정책 페이지는 정적 생성
'/': { prerender: true },
'/about': { prerender: true },
'/privacy': { prerender: true },

// 제품 목록: 10분마다 갱신
'/products': { swr: 600 },
'/products/**': { swr: 3600 },

// 장바구니/주문: SSR만 (캐시 없음)
'/cart': { ssr: true, headers: { 'cache-control': 'no-store' } },
'/checkout': { ssr: true, headers: { 'cache-control': 'no-store' } },

// 마이페이지: CSR (개인화 데이터)
'/account/**': { ssr: false },
'/orders/**': { ssr: false },

// API
'/api/public/**': { cors: true, cache: { maxAge: 60 } },
'/api/user/**': { headers: { 'cache-control': 'no-store' } },
}
},

// Tailwind CSS 설정
tailwindcss: {
configPath: '~/tailwind.config.ts',
exposeConfig: true,
},

// 이미지 최적화
image: {
quality: 80,
formats: ['avif', 'webp'],
domains: ['cdn.example.com'],
presets: {
thumbnail: {
modifiers: { width: 300, height: 300, fit: 'cover' }
},
product: {
modifiers: { width: 600, height: 600, fit: 'cover' }
}
}
},

experimental: {
typedPages: true,
},
})

app.vue 루트 컴포넌트

<!-- app.vue -->
<template>
<div>
<!-- 레이아웃 적용 -->
<NuxtLayout>
<!-- 페이지 렌더링 -->
<NuxtPage />
</NuxtLayout>
</div>
</template>

<script setup lang="ts">
// 전역 에러 핸들러
const { $toast } = useNuxtApp()

// 전역 상태 초기화
const authStore = useAuthStore()
await authStore.initialize()
</script>

레이아웃 설정

<!-- layouts/default.vue -->
<template>
<div class="min-h-screen flex flex-col">
<TheHeader />

<main class="flex-1 container mx-auto px-4 py-8">
<slot />
</main>

<TheFooter />

<!-- 전역 토스트 알림 -->
<ToastContainer />
</div>
</template>

<script setup lang="ts">
// 레이아웃 레벨 SEO
useHead({
titleTemplate: (title) => title ? `${title} | 쇼핑몰` : '쇼핑몰',
})
</script>
<!-- layouts/checkout.vue -->
<template>
<div class="min-h-screen bg-gray-50">
<CheckoutHeader />

<main class="max-w-2xl mx-auto px-4 py-8">
<slot />
</main>

<!-- 체크아웃 레이아웃에는 푸터 없음 -->
</div>
</template>

TypeScript 설정

Nuxt 3는 TypeScript를 기본으로 지원합니다.

tsconfig.json

Nuxt가 .nuxt/tsconfig.json을 자동 생성하므로, 루트의 tsconfig.json은 이를 확장만 하면 됩니다.

{
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true
}
}

타입 선언 파일

// types/index.ts
export interface User {
id: number
name: string
email: string
role: 'user' | 'admin'
createdAt: string
}

export interface Product {
id: number
name: string
price: number
description: string
images: string[]
stock: number
category: Category
}

export interface Category {
id: number
name: string
slug: string
}

export interface ApiResponse<T> {
data: T
message: string
success: boolean
}
// types/nuxt.d.ts — Nuxt 타입 확장
declare module 'nuxt/schema' {
interface RuntimeConfig {
stripe: {
secretKey: string
}
db: {
url: string
}
}

interface PublicRuntimeConfig {
appName: string
stripe: {
publishableKey: string
}
}
}

export {}

고수 팁

1. 레이어(Layers) 시스템으로 모듈화

// nuxt.config.ts
export default defineNuxtConfig({
// 다른 Nuxt 프로젝트나 패키지를 레이어로 확장
extends: [
'../shared-layer', // 공유 컴포넌트/컴포저블
'github:my-org/nuxt-ui-kit', // GitHub의 레이어
'@my-company/nuxt-auth', // npm 패키지 레이어
]
})

2. 훅(Hooks)으로 빌드 프로세스 커스터마이즈

// nuxt.config.ts
export default defineNuxtConfig({
hooks: {
// 빌드 완료 후 실행
'build:done'(nuxt) {
console.log('빌드 완료!')
},

// 페이지 생성 시 실행
'pages:extend'(pages) {
// 프로그래밍 방식으로 라우트 추가
pages.push({
name: 'custom-page',
path: '/custom',
file: '~/pages/custom.vue',
})
},

// Vite 설정 확장
'vite:extendConfig'(config, { isClient, isServer }) {
if (isClient) {
config.resolve!.alias!['#build/runtime'] = '~/runtime'
}
}
}
})

3. 앱 설정(app.config.ts)과 런타임 설정 구분

// app.config.ts — UI/UX 설정 (클라이언트에서 런타임 변경 가능)
export default defineAppConfig({
theme: {
primaryColor: '#3b82f6',
borderRadius: 'rounded-lg',
},
ui: {
notifications: {
position: 'top-right',
timeout: 3000,
}
}
})
<!-- 컴포넌트에서 사용 -->
<script setup lang="ts">
const appConfig = useAppConfig()
console.log(appConfig.theme.primaryColor) // '#3b82f6'
</script>

4. 개발/프로덕션 환경 분기

// nuxt.config.ts
export default defineNuxtConfig({
$development: {
// 개발 환경에서만 적용
devtools: { enabled: true },
debug: true,
},
$production: {
// 프로덕션 환경에서만 적용
nitro: {
compressPublicAssets: true,
}
}
})

정리

Nuxt 3의 환경 설정은 처음에 복잡해 보일 수 있지만, 대부분의 설정은 기본값이 이미 잘 되어 있어 nuxt.config.ts에서 필요한 부분만 오버라이드하면 됩니다.

핵심 포인트:

  • 디렉토리 구조: 각 폴더가 특별한 역할을 가지며, Nuxt가 자동으로 인식
  • nuxt.config.ts: 모든 설정의 중심, routeRules로 페이지별 렌더링 전략 설정
  • 환경 변수: runtimeConfig로 안전하게 관리, 서버/클라이언트 접근 분리
  • TypeScript: 기본 지원, 타입 선언으로 더욱 안전한 개발

다음 챕터에서는 Nuxt의 핵심 기능인 데이터 페칭을 상세히 알아보겠습니다.

Advertisement