# 내가 읽으려고 만든 Nuxt 입문하기

> Vue3 + Next.js App Router 경험자 기준으로 작성.

## 1. 핵심 구조 차이

```javascript
Next.js App Router      Nuxt3
─────────────────────────────────────────────
app/                    pages/
app/layout.tsx          layouts/
app/page.tsx            pages/index.vue
app/api/route.ts        server/api/
middleware.ts           middleware/
'use client'            (없음 — 대신 <ClientOnly>)
Server Component        (없음 — 모든 컴포넌트는 Universal)
export const metadata   useSeoMeta() / useHead()
loading.tsx             <NuxtLoadingIndicator>
error.tsx               error.vue
```

---

## 2. 파일 기반 라우팅

개념은 같지만 파일명과 폴더 구조 방식이 다르다.

### Next.js App Router

폴더가 라우트 세그먼트이고, 폴더 안의 `page.tsx`가 실제 페이지.

```javascript
app/
├── page.tsx              →  /
├── about/
│   └── page.tsx          →  /about
└── users/
    ├── page.tsx          →  /users
    └── [id]/
        └── page.tsx      →  /users/:id
```

### Nuxt3

`.vue` 파일 자체가 라우트. 폴더 구조도 동일하게 지원.

```javascript
pages/
├── index.vue             →  /
├── about.vue             →  /about
└── users/
    ├── index.vue         →  /users
    └── [id].vue          →  /users/:id
```

### 동적 파라미터 접근

```javascript
<!-- pages/users/[id].vue -->
<script setup>
const route = useRoute()
const id = route.params.id  // /users/123 → "123"
</script>
```

### 라우터 이동

```javascript
<script setup>
const router = useRouter()
router.push('/about')
router.push({ name: 'users-id', params: { id: 1 } })
// 라우트 name은 파일 경로를 '-'로 연결 → users/[id].vue = 'users-id'
</script>

<template>
  <!-- Next.js의 <Link> = Nuxt3의 <NuxtLink> -->
  <NuxtLink to="/about">소개</NuxtLink>
  <NuxtLink :to="{ name: 'users-id', params: { id: 1 } }">유저 1</NuxtLink>
</template>
```

---

## 3. 컴포넌트 모델 — 가장 큰 차이점

이 부분이 Next.js App Router와 Nuxt3의 가장 근본적인 차이다.

### Next.js App Router: Server Component vs Client Component

```javascript
// Server Component (기본값) — 서버에서만 실행, JS 번들에 포함 안 됨
// useState, useEffect 등 React 훅 사용 불가
async function UserList() {
  const users = await db.getUsers()  // 직접 DB 접근 가능
  return <ul>{users.map(u => <li>{u.name}</li>)}</ul>
}

// Client Component — 'use client' 선언 필요
'use client'
function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}
```

### Nuxt3: Universal Component (단일 모델)

Nuxt3에는 Server Component / Client Component 구분이 없다.
모든 컴포넌트는 **SSR 시 서버에서 실행되고, hydration 후 클라이언트에서도 실행**된다.
`<script setup>` 하나로 모든 것을 처리한다.

```javascript
<script setup>
// 이 코드는 SSR 시 서버에서 실행되고, 클라이언트에서도 실행됨
// → 브라우저 전용 API (window, localStorage 등)는 직접 쓰면 SSR에서 에러 남
const { data } = await useFetch('/api/users')  // SSR에서 실행
</script>

<template>
  <ul>
    <li v-for="user in data" :key="user.id">{{ user.name }}</li>
  </ul>
</template>
```

### 브라우저 전용 코드 처리

```javascript
<script setup>
// ❌ SSR에서 에러 발생
const saved = localStorage.getItem('key')

// ✅ 클라이언트에서만 실행
onMounted(() => {
  const saved = localStorage.getItem('key')
})

// ✅ 또는 환경 분기
if (import.meta.client) {
  const saved = localStorage.getItem('key')
}
</script>

<template>
  <!-- ✅ 컴포넌트 단위로 클라이언트 전용 렌더링 -->
  <ClientOnly>
    <MyBrowserOnlyComponent />
  </ClientOnly>
</template>
```

---

## 4. Auto-imports

Nuxt3의 핵심 기능. `import` 없이 사용 가능한 것들:

- `components/` 폴더의 모든 컴포넌트

- `composables/` 폴더의 모든 composable

- Vue3 Composition API (`ref`, `computed`, `watch` 등)

- Nuxt3 내장 composable (`useRoute`, `useRouter`, `useFetch` 등)

```javascript
<script setup>
// import 없이 사용 가능
const count = ref(0)
const route = useRoute()
const { data } = await useFetch('/api/users')
</script>

<template>
  <!-- components/MyButton.vue → <MyButton> 자동 등록 -->
  <MyButton @click="count++" />
</template>
```

단, `composables/`와 `components/` 외부 파일에서는 직접 import 필요:

```javascript
// 일반 .ts 파일
import { ref } from 'vue'
```

---

## 5. 데이터 페칭

### Next.js App Router: Server Component에서 직접 fetch

```javascript
// Server Component — async/await로 직접 데이터 페칭
export default async function Page() {
  // SSR (cache: 'no-store')
  const data = await fetch('https://api.example.com/users', {
    cache: 'no-store',
  }).then(r => r.json())

  return <UserList users={data} />
}
```

### Nuxt3: useFetch

SSR/클라이언트 환경에 관계없이 동일한 API를 사용한다.

```javascript
<script setup>
// 기본 사용 — SSR에서 실행됨
const { data, pending, error, refresh } = await useFetch('/api/users')

// 타입 지정
const { data } = await useFetch<User[]>('/api/users')

// POST 요청
const { data } = await useFetch('/api/users', {
  method: 'POST',
  body: { name: '김현우' },
})

// 동적 URL — ref가 바뀌면 자동으로 재요청
const userId = ref(1)
const { data } = await useFetch(() => `/api/users/${userId.value}`)
</script>
```

### useFetch 주요 옵션

```javascript
<script setup>
const { data } = await useFetch('/api/users', {
  // lazy: true → SSR에서 이 요청을 기다리지 않고 페이지를 먼저 반환
  //               클라이언트에서 완료됨. pending 상태로 시작.
  lazy: true,

  // server: false → 클라이언트에서만 실행 (SSR 완전히 스킵)
  server: false,

  // pick → 응답에서 필요한 필드만 추출 (payload 최적화)
  pick: ['id', 'name'],
})
</script>
```

### useAsyncData — 외부 SDK나 복잡한 로직

```javascript
<script setup>
// useFetch는 내부적으로 useAsyncData + $fetch의 조합
// 외부 SDK, 복잡한 변환 로직이 필요할 때 useAsyncData 직접 사용
const { data } = await useAsyncData('posts', async () => {
  const posts = await someExternalSDK.getPosts()
  return posts.filter(p => p.published)
})
</script>
```

### $fetch — 이벤트 핸들러에서 사용

```javascript
<script setup>
// useFetch/useAsyncData는 setup의 SSR 타이밍에 실행되도록 설계됨
// 버튼 클릭 등 이벤트 핸들러에서는 $fetch 사용
async function submit() {
  const result = await $fetch('/api/submit', {
    method: 'POST',
    body: formData.value,
  })
}
</script>
```

### 선택 기준 정리

| 상황 | 선택 |
| --- | --- |
| 페이지 로드 시 URL로 API 요청 | `useFetch` |
| 외부 SDK / DB 직접 호출 | `useAsyncData` |
| 클릭/폼 제출 등 이벤트 | `$fetch` |
| URL이 ref에 의존해 동적으로 변함 | `useFetch(() => url)` |

---

## 6. 레이아웃

### Next.js App Router: 폴더 내 layout.tsx (중첩 레이아웃 자동 적용)

```javascript
// app/layout.tsx → 모든 페이지에 적용
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Header />
        {children}
        <Footer />
      </body>
    </html>
  )
}
```

### Nuxt3: layouts/ 폴더 + 명시적 적용

```javascript
<!-- layouts/default.vue → 기본 레이아웃 -->
<template>
  <div>
    <AppHeader />
    <slot />  <!-- 페이지 내용이 여기에 렌더링 -->
    <AppFooter />
  </div>
</template>
```

`app.vue`에서 `<NuxtLayout>`으로 활성화:

```javascript
<!-- app.vue -->
<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>
```

### 레이아웃 변경

```javascript
<!-- pages/admin/dashboard.vue -->
<script setup>
definePageMeta({
  layout: 'admin',  // layouts/admin.vue 사용
})
</script>
```

Next.js App Router는 폴더 계층으로 레이아웃이 자동 적용되고,
Nuxt3는 `definePageMeta`로 명시적으로 지정한다는 차이가 있다.

---

## 7. 미들웨어

### Next.js App Router: 루트의 middleware.ts (Edge Runtime)

```javascript
// middleware.ts — 모든 요청에 실행 (Edge Runtime)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')
  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
}

export const config = {
  matcher: ['/dashboard/:path*'],
}
```

### Nuxt3: middleware/ 폴더 (라우트 미들웨어)

**클라이언트 네비게이션 + 서버 최초 요청** 양쪽에서 실행된다.

```javascript
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const token = useCookie('auth-token')

  if (!token.value) {
    return navigateTo('/login')
  }
})
```

페이지에서 적용:

```javascript
<script setup>
definePageMeta({
  middleware: 'auth',
  // middleware: ['auth', 'role'],  // 복수 지정 가능
})
</script>
```

### 글로벌 미들웨어 (모든 라우트에 자동 적용)

```javascript
// middleware/logger.global.ts
// 파일명에 '.global' 붙이면 전체 라우트에 자동 적용
export default defineNuxtRouteMiddleware((to) => {
  console.log('이동:', to.path)
})
```

### Next.js vs Nuxt3 미들웨어 차이

|  | Next.js App Router | Nuxt3 |
| --- | --- | --- |
| 실행 환경 | Edge Runtime (서버만) | 서버 최초 요청 + 클라이언트 네비게이션 |
| 파일 위치 | 루트 `middleware.ts` 1개 | `middleware/` 폴더, 파일별 분리 |
| 적용 범위 | `matcher` 설정 | `definePageMeta` 또는 `.global.ts` |

---

## 8. 서버 라우트

### Next.js App Router: app/api/route.ts

```javascript
// app/api/users/route.ts
export async function GET() {
  const users = await db.getUsers()
  return Response.json(users)
}

export async function POST(request: Request) {
  const body = await request.json()
  const user = await db.createUser(body)
  return Response.json(user, { status: 201 })
}
```

### Nuxt3: server/api/ (Nitro)

```javascript
// server/api/users.get.ts  → GET /api/users
export default defineEventHandler(async (event) => {
  const users = await db.getUsers()
  return users  // 자동으로 JSON 직렬화
})

// server/api/users.post.ts  → POST /api/users
export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  const user = await db.createUser(body)
  return user
})

// server/api/users/[id].get.ts  → GET /api/users/:id
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')
  return await db.getUserById(id)
})
```

### 파일명 컨벤션

```javascript
server/api/users.ts          →  /api/users (모든 메서드)
server/api/users.get.ts      →  GET /api/users
server/api/users.post.ts     →  POST /api/users
server/api/users/[id].get.ts →  GET /api/users/:id
server/routes/sitemap.xml.ts →  /sitemap.xml  (/api 접두사 없음)
```

### 유틸 함수

```javascript
export default defineEventHandler(async (event) => {
  // 쿼리 파라미터: GET /api/search?q=nuxt
  const query = getQuery(event)   // { q: 'nuxt' }

  // 요청 바디 (POST)
  const body = await readBody(event)

  // 헤더
  const authHeader = getHeader(event, 'authorization')

  // 쿠키
  const token = getCookie(event, 'auth-token')

  // 상태 코드 설정
  setResponseStatus(event, 201)
})
```

---

## 9. 상태 관리

### useState — SSR-safe 전역 상태

```javascript
// ref()는 서버에서 생성된 값이 클라이언트로 전달되지 않음
// useState는 서버 → 클라이언트 hydration을 지원
const count = useState('global-count', () => 0)
// 두 번째 인자는 초기값 팩토리 (최초 1회만 실행)
```

같은 키를 사용하면 어디서 호출해도 같은 상태를 공유한다:

```javascript
// composables/useCounter.ts
export const useCounter = () => useState('counter', () => 0)
```

### Pinia — 복잡한 상태

```javascript
npx nuxi module add pinia
```

```javascript
// stores/user.ts
export const useUserStore = defineStore('user', () => {
  const user = ref<User | null>(null)
  const isLoggedIn = computed(() => !!user.value)

  async function login(credentials: Credentials) {
    user.value = await $fetch('/api/auth/login', {
      method: 'POST',
      body: credentials,
    })
  }

  return { user, isLoggedIn, login }
})
```

```javascript
<script setup>
// stores/는 Auto-import 대상이 아님 → 직접 import 필요
import { useUserStore } from '~/stores/user'
const userStore = useUserStore()
</script>
```

`@pinia/nuxt` 모듈이 SSR hydration을 자동으로 처리하므로 별도 설정 불필요.

---

## 10. SEO & Meta

### Next.js App Router: export const metadata

```javascript
// 정적
export const metadata: Metadata = {
  title: '페이지 제목',
  description: '페이지 설명',
  openGraph: {
    title: 'OG 제목',
    images: ['/og.png'],
  },
}

// 동적
export async function generateMetadata({ params }): Promise<Metadata> {
  const post = await fetchPost(params.id)
  return { title: post.title }
}
```

### Nuxt3: useHead / useSeoMeta

```javascript
<script setup>
// 일반 메타
useHead({
  title: '페이지 제목',
  meta: [{ name: 'description', content: '설명' }],
})

// OG/SEO 전용 (타입 안전)
useSeoMeta({
  title: '페이지 제목',
  description: '설명',
  ogTitle: 'OG 제목',
  ogDescription: 'OG 설명',
  ogImage: 'https://example.com/og.png',
  twitterCard: 'summary_large_image',
})

// 동적 (ref 사용 가능)
const { data: post } = await useFetch(`/api/posts/${route.params.id}`)
useSeoMeta({
  title: () => post.value?.title,  // 데이터 로드 후 자동 업데이트
})
</script>
```

전역 기본값:

```javascript
// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      titleTemplate: '%s | 사이트명',
      meta: [{ name: 'description', content: '기본 설명' }],
    },
  },
})
```

---

## 11. 환경변수

### Next.js App Router

```javascript
NEXT_PUBLIC_API_URL=...  → 클라이언트 노출 (NEXT_PUBLIC_ 접두사)
API_SECRET=...           → 서버에서만 접근 가능
```

### Nuxt3: runtimeConfig

```javascript
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    apiSecret: '',          // 서버에서만 접근 가능
    public: {
      apiBase: '',          // 클라이언트에도 노출
    },
  },
})
```

`.env` 파일의 `NUXT_` 접두사 변수가 자동으로 매핑됨:

```javascript
# .env
NUXT_API_SECRET=my-secret               → runtimeConfig.apiSecret
NUXT_PUBLIC_API_BASE=https://api.com    → runtimeConfig.public.apiBase
```

```javascript
<script setup>
const config = useRuntimeConfig()
// 클라이언트: config.public.apiBase 접근 가능
// 서버: config.apiSecret도 접근 가능
</script>
```

---

## 12. 에러 처리

### Next.js App Router: error.tsx (Client Boundary)

```javascript
// app/error.tsx
'use client'
export default function Error({ error, reset }) {
  return (
    <div>
      <h2>{error.message}</h2>
      <button onClick={reset}>재시도</button>
    </div>
  )
}
```

### Nuxt3: error.vue

```javascript
<!-- error.vue (app.vue와 같은 레벨) -->
<script setup>
const props = defineProps<{ error: { statusCode: number; message: string } }>()

function handleError() {
  clearError({ redirect: '/' })
}
</script>

<template>
  <div>
    <h1>{{ error.statusCode }}</h1>
    <p>{{ error.message }}</p>
    <button @click="handleError">홈으로</button>
  </div>
</template>
```

에러 발생시키기:

```javascript
// 페이지/컴포넌트에서
throw createError({ statusCode: 404, message: 'Not found' })

// fatal: true → error.vue로 이동 (fatal: false는 컴포넌트 내에서만 처리)
throw createError({ statusCode: 500, message: 'Server error', fatal: true })
```

---

## 13. 렌더링 전략

### Next.js App Router

```javascript
// SSR (매 요청마다)
const data = await fetch(url, { cache: 'no-store' })

// SSG (빌드 시 생성)
const data = await fetch(url)  // 기본값이 캐시됨 (Next.js 14 이하)
// Next.js 15부터는 기본값이 no-store로 변경됨

// ISR
const data = await fetch(url, { next: { revalidate: 60 } })

// 정적 경로 생성
export async function generateStaticParams() {
  return [{ id: '1' }, { id: '2' }]
}
```

### Nuxt3: routeRules + nuxt generate

```javascript
// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/':           { prerender: true },    // SSG
    '/blog/**':    { isr: 3600 },          // ISR (3600초마다 재생성)
    '/admin/**':   { ssr: false },         // CSR (서버 렌더링 안 함)
    '/api/**':     { cors: true },
  },
})
```

전체 SSG로 빌드:

```javascript
npx nuxi generate
```

`useFetch`의 SSR 제어:

```javascript
<script setup>
// SSR에서 기다리지 않고 클라이언트에서 페칭 (lazy loading)
const { data, pending } = await useFetch('/api/users', { lazy: true })

// 완전히 클라이언트에서만 실행
const { data } = await useFetch('/api/users', { server: false })
</script>
```

---

## 14. nuxt.config.ts 핵심 설정

```javascript
export default defineNuxtConfig({
  // SSR 여부 (기본값 true)
  ssr: true,

  // 개발자 도구
  devtools: { enabled: true },

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

  // 런타임 환경변수
  runtimeConfig: {
    apiSecret: '',
    public: {
      apiBase: '',
    },
  },

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

  // 라우트별 렌더링 전략
  routeRules: {
    '/': { prerender: true },
    '/blog/**': { isr: 3600 },
    '/admin/**': { ssr: false },
  },

  // Vite 설정 확장
  vite: {
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: '@use "~/assets/scss/variables" as *;',
        },
      },
    },
  },

  // TypeScript strict 모드
  typescript: {
    strict: true,
  },
})
```

---

## 15. 자주 쓰는 패턴 모음

### definePageMeta

```javascript
<script setup>
definePageMeta({
  layout: 'admin',
  middleware: ['auth'],
})
</script>
```

### 쿠키 (서버-클라이언트 모두 동작)

```javascript
<script setup>
const token = useCookie('auth-token', {
  maxAge: 60 * 60 * 24 * 7,  // 7일
  httpOnly: true,
  secure: true,
})

token.value = 'my-token'  // 자동으로 쿠키에 저장됨
token.value = null        // 쿠키 삭제
</script>
```

### NuxtLink 활성 클래스

```javascript
<template>
  <NuxtLink
    to="/about"
    activeClass="text-blue-500"
    exactActiveClass="font-bold"
  >
    소개
  </NuxtLink>
</template>
```

### 플러그인 — 전역 함수/라이브러리 등록

```javascript
// plugins/my-plugin.ts
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(SomeVuePlugin)

  return {
    provide: {
      formatDate: (date: Date) => date.toLocaleDateString('ko-KR'),
    },
  }
})
```

```javascript
<script setup>
const { $formatDate } = useNuxtApp()
</script>
```

클라이언트/서버 전용 플러그인:

```javascript
plugins/analytics.client.ts  → 클라이언트에서만 실행
plugins/db.server.ts          → 서버에서만 실행
```

### 중첩 라우트 (Nested Routes)

```javascript
pages/
└── users/
    ├── index.vue       →  /users
    └── [id].vue        →  /users/:id
```

부모 페이지 안에 `<NuxtPage />`를 넣으면 중첩 렌더링됨:

```javascript
<!-- pages/users.vue -->
<template>
  <div>
    <UserSidebar />
    <NuxtPage />  <!-- /users/:id 내용이 여기에 렌더링 -->
  </div>
</template>
```

---

## 빠른 참조

```javascript
composable                  역할
────────────────────────────────────────────────
useFetch(url)               SSR 데이터 페칭
useAsyncData(key, fn)       커스텀 로직 SSR 페칭
$fetch(url)                 이벤트 핸들러에서 페칭
useRoute()                  현재 라우트 정보
useRouter()                 라우터 이동
useCookie(name)             쿠키 읽기/쓰기
useRuntimeConfig()          환경변수
useState(key, init)         SSR-safe 전역 상태
useHead({})                 페이지 헤드 설정
useSeoMeta({})              SEO / OG 태그
useNuxtApp()                플러그인 ($xxx 접근)
navigateTo(path)            미들웨어에서 리다이렉트
createError({})             에러 생성/throw
```

For the site tree, see the [root Markdown](https://slashpage.com/timmy.md).
