돌아가기 Vue 3 + Nuxt 3 코딩 컨벤션
import { FileTree } from '@astrojs/starlight/components';

## 1. 프로젝트 디렉터리 구조
Nuxt 3는 다수의 폴더를 자동 스캔하여 기능을 제공합니다. **폴더명은 kebab-case**를 권장합니다.

### 1.1 디렉터리 트리 예시


<FileTree>
- my-app
  - app.vue               # 최상위 앱 컴포넌트
  - nuxt.config.ts        # Nuxt 설정 파일
  - assets/               # 스타일·폰트·이미지 등(빌드 도구가 처리)
  - components/           # 재사용 가능한 Vue 컴포넌트 (자동 임포트)
  - composables/          # 재사용 가능한 composition 함수 (자동 임포트)
  - layouts/              # 페이지 레이아웃 (비동기 로드)
  - middleware/           # 클라이언트 라우트 미들웨어
  - pages/                # 파일 기반 라우팅 페이지 컴포넌트
  - plugins/              # Nuxt 플러그인 (자동 등록)
  - stores/               # Pinia 상태 관리 스토어 (자동 임포트)
  - server/               # 서버 API / routes / middleware / plugins
  - utils/                # 일반 유틸리티 (자동 임포트)
  - public/               # 빌드 미처리 정적 파일 (robots.txt 등)
  - types/                # TypeScript 타입 정의 (선택)
</FileTree>

### 1.2 폴더별 상세 가이드

#### assets/
빌드 도구가 처리하는 정적 자산을 저장합니다.
- CSS, Sass, 이미지, 폰트 등
- 코드에서 직접 임포트
- Vite/Webpack에 의해 최적화됨

```vue
<script setup>
import logo from '~/assets/images/logo.png'
</script>

<style>
@import '~/assets/styles/variables.scss';
</style>
```

#### components/
Vue 컴포넌트를 저장하며 **자동으로 임포트**됩니다.

**네이밍 규칙:**
- 하위 폴더 구조가 컴포넌트 이름을 결정
- 예: `components/base/foo/Button.vue` → `<BaseFooButton />`

**특수 기능:**
- **전역 컴포넌트**: `.global.vue` 접미사 (과용 금지)
- **지연 로딩**: `Lazy` 접두사 사용 → `<LazyFooBar />`
- **클라이언트 전용**: `.client.vue` 접미사
- **서버 전용**: `.server.vue` 접미사

```vue
<!-- 지연 로딩 예제 -->
<template>
  <div>
    <!-- 필요할 때만 로드 -->
    <LazyHeavyComponent v-if="showComponent" />
  </div>
</template>
```

#### composables/
Composition API 기반 재사용 가능한 로직을 저장합니다.

**규칙:**
- 파일명: `useSomething.ts` 형태 권장
- 기본적으로 최상위 파일만 스캔
- 하위 폴더 사용 시 `nuxt.config.ts`에서 설정 필요

```typescript
// nuxt.config.ts - 하위 폴더 스캔 설정
export default defineNuxtConfig({
  imports: {
    dirs: ['composables/**']
  }
})
```

#### layouts/
페이지의 공통 레이아웃을 정의합니다.

**중요사항:**
- 비동기 로드됨
- 이름은 kebab-case로 변환
- **단일 루트 요소** 필수 (루트가 `<slot />`이면 안 됨)

```vue
<!-- layouts/default.vue -->
<template>
  <div>
    <Header />
    <main>
      <slot />
    </main>
    <Footer />
  </div>
</template>
```

#### middleware/
라우트 네비게이션 가드를 정의합니다.

**세 가지 유형:**
1. **익명 미들웨어**: 페이지 내부에서 직접 정의
2. **명명 미들웨어**: 파일명으로 참조
3. **전역 미들웨어**: `.global.ts` 접미사

```typescript
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const user = useUserStore()
  if (!user.isAuthenticated) {
    return navigateTo('/login')
  }
})
```

```vue
<!-- pages/dashboard.vue -->
<script setup>
definePageMeta({
  middleware: 'auth'
})
</script>
```

#### pages/
파일 기반 라우팅 시스템을 사용합니다.

**라우팅 패턴:**

| 파일 | 라우트 |
|------|--------|
| `pages/index.vue` | `/` |
| `pages/about.vue` | `/about` |
| `pages/posts/[id].vue` | `/posts/:id` (동적) |
| `pages/posts/[[id]].vue` | `/posts/:id?` (선택적) |
| `pages/[...slug].vue` | `/*` (캐치올) |
| `pages/(marketing)/about.vue` | `/about` (그룹화) |

**단일 루트 요소 필수**

#### stores/
Pinia를 사용한 상태 관리 스토어입니다.

**Setup 스토어 (권장)**

```typescript
// stores/user.ts
export const useUserStore = defineStore('user', () => {
  const name = ref('')
  const email = ref('')

  function updateProfile(newName: string, newEmail: string) {
    name.value = newName
    email.value = newEmail
  }

  return { name, email, updateProfile }
})
```

**Options 스토어**

```typescript
// stores/counter.ts
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++
    }
  }
})
```

**useState vs Pinia 선택 가이드**

| 상황 | 권장 솔루션 |
|------|------------|
| 단순 전역 상태 (테마, 로케일 등) | `useState` |
| 컴포넌트 간 공유되는 단일 값 | `useState` |
| 복잡한 상태 + 여러 액션 | Pinia |
| 상태 지속성 (localStorage 등) | Pinia |
| DevTools 디버깅 필요 | Pinia |

```typescript
// ✅ 간단한 상태: useState 사용
export const useTheme = () => useState<'light' | 'dark'>('theme', () => 'light')
export const useLocale = () => useState<string>('locale', () => 'ko-KR')

// ✅ 복잡한 상태: Pinia 사용
export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])
  const total = computed(() => items.value.reduce((sum, item) => sum + item.price, 0))

  function addItem(item: CartItem) { /* ... */ }
  function removeItem(id: string) { /* ... */ }
  function checkout() { /* ... */ }

  return { items, total, addItem, removeItem, checkout }
})
```

#### server/
서버 사이드 API와 로직을 정의합니다.

**구조:**
- `server/api/` → `/api/*` 엔드포인트
- `server/routes/` → `/api` 없는 라우트
- `server/middleware/` → 서버 미들웨어
- `server/utils/` → 서버 유틸리티

```typescript
// server/api/users.ts
export default defineEventHandler(async (event) => {
  const users = await fetchUsers()
  return users
})

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

#### utils/
일반 유틸리티 함수를 저장하며 **자동 임포트**됩니다.

```typescript
// utils/format.ts
export const formatDate = (date: Date) => {
  return date.toLocaleDateString('ko-KR')
}

// 자동 임포트되어 어디서든 사용 가능
// const formatted = formatDate(new Date())
```

#### public/
빌드 처리 없이 그대로 제공되는 정적 파일입니다.
- `public/robots.txt` → `/robots.txt`
- `public/favicon.ico` → `/favicon.ico`

---

## 2. 파일 및 컴포넌트 명명 규칙

### 2.1 컴포넌트 파일 명
- **PascalCase** 또는 **kebab-case** 중 **하나를 선택해 일관성** 있게.
- **베이스 컴포넌트**: `Base`, `App`, `V` 접두사 사용(예: `BaseButton.vue`, `BaseTable.vue`).
  - HTML 요소/다른 베이스 컴포넌트/외부 UI만 포함, **글로벌 상태 금지**.
  - 접두사 정렬로 에디터 탐색 효율 ↑
- **부모-자식 관계**가 강한 경우, **부모 이름을 접두사**로(예: `TodoListItemButton.vue`).
- **일반 규칙**: 상위 개념 → 하위 개념 순(예: `SearchButtonClear.vue`).
- **HTML 요소 충돌 방지**를 위해 **두 단어 이상**으로 명명.

### 2.2 페이지 파일 명
- `pages/index.vue` → `/` 루트. 하위 폴더의 `index.vue`는 해당 디렉터리의 기본 경로.
- **동적 라우트**: `[id].vue` / **선택적 파라미터**: `[[param]].vue`
- **혼합 세그먼트**: `users-[group]/[id].vue`
- **캐치올**: `[...slug].vue`
- **라우트 그룹**: `pages/(group)/...` (URL 영향 없음)
- 페이지는 **단일 루트 요소 필수**.

### 2.3 레이아웃 파일 명
- 레이아웃 이름은 **kebab-case**로 변환. 파일명과 일치.
  - 예: `layouts/custom.vue` → `custom` 레이아웃
- 중첩 폴더는 경로 기반 이름 부여
  - 예: `~/layouts/desktop/default.vue` → `desktop-default`
- **단일 루트 요소** 필요(루트 `<slot />` 금지).

### 2.4 컴포저블·유틸리티·스토어 파일 명
- **컴포저블**: `composables/useSomething.ts`(함수명 `useSomething`). camelCase/kebab-case 파일명 허용하나 **노출 함수는 camelCase**.
- **유틸리티**: `utils/random-entry.ts` → `randomEntry()`.
- **Pinia 스토어**: `stores/user.ts` + `defineStore('user', ...)` → 내보내기 함수 `useUserStore`.
  - 각 파일 **1 스토어** 권장. 여러 스토어 사용 가능.

### 2.5 서버 API·미들웨어·플러그인 파일 명
- **API**: `server/api/hello.ts` → `/api/hello`
  - 동적: `[name].ts`, 메서드별: `.get.ts`, `.post.ts`, ...
- **server routes**: `/api` 접두사 제거 필요 시 `server/routes/*`
- **server middleware**: `server/middleware/log.ts` (모든 요청 전 실행)
- **server plugins**: `server/plugins/myPlugin.ts` (Nitro 플러그인)
- **클라이언트 미들웨어**: `app/middleware/*.ts` (kebab-case, 전역은 `.global`)

---

## 3. 함수·변수·컴포저블 네이밍 규칙

### 3.1 일반 규칙
- **camelCase** 사용(JS/TS 함수·변수·반응형 상태 포함).
- **불린 변수**: `is*`, `has*`, `should*` 접두사(예: `isAuthenticated`, `hasPermission`).
- **이벤트 핸들러**: `handle*`, `on*`(예: `handleSubmit`, `onClick`).
- **비동기/데이터 패칭**: `fetch*`, `load*`, `get*`(예: `fetchUserProfile`, `loadPosts`).
- **컴포저블**: `use*`(예: `useAuth`, `useFetchData`).
- **Pinia 스토어**: `use*Store`(예: `useUserStore`).

### 3.2 Vue 컴포넌트 스크립트 작성
- 기본적으로 **`<script setup>`** 사용(간결/타입 추론 우수).
- 각 파일 **하나의 컴포넌트**, `export default` 사용.
- 화살표 함수 선호, 불필요한 중괄호·세미콜론 지양.

**예제: 기본 컴포넌트 구조**

```vue
<script setup lang="ts">
// Nuxt 3에서는 ref, computed, onMounted 등이 auto-import됨
// import { ref, computed, onMounted } from 'vue' ← 불필요

// Props 정의
const props = defineProps<{
  title: string
  count?: number
}>()

// Emits 정의 (Vue 3.3+ named tuple 문법 - 권장)
const emit = defineEmits<{
  update: [value: number]
  delete: []
}>()

// 또는 기존 call signature 문법 (Vue 3.3 이전 호환)
// const emit = defineEmits<{
//   (e: 'update', value: number): void
//   (e: 'delete'): void
// }>()

// 반응형 상태
const localCount = ref(props.count ?? 0)

// Computed
const doubleCount = computed(() => localCount.value * 2)

// 메서드
function increment() {
  localCount.value++
  emit('update', localCount.value)
}

// 라이프사이클
onMounted(() => {
  console.log('Component mounted')
})
</script>

<template>
  <div>
    <h2>{{ title }}</h2>
    <p>Count: {{ localCount }}</p>
    <p>Double: {{ doubleCount }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

<style scoped>
button {
  padding: 0.5rem 1rem;
  background-color: #42b883;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>
```

#### defineModel을 활용한 양방향 바인딩 (Vue 3.4+)

Vue 3.4부터 도입된 `defineModel()` 매크로는 `v-model` 양방향 바인딩을 간편하게 구현합니다.

**기본 사용법**

```vue
<script setup lang="ts">
// 기본 v-model 바인딩 (modelValue prop)
const model = defineModel()

// 타입 지정
const model = defineModel<string>()

// 필수 + 타입 지정
const model = defineModel<string>({ required: true })

// 기본값 지정
const model = defineModel({ default: 0 })
</script>

<template>
  <input v-model="model" />
</template>
```

**Named Model (v-model:name)**

```vue
<script setup lang="ts">
// v-model:title 바인딩
const title = defineModel('title')

// 여러 v-model 지원
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>

<template>
  <input v-model="firstName" placeholder="이름" />
  <input v-model="lastName" placeholder="성" />
</template>
```

**부모 컴포넌트에서 사용**

```vue
<template>
  <!-- 기본 v-model -->
  <MyInput v-model="searchText" />

  <!-- Named v-model -->
  <UserNameForm
    v-model:first-name="user.firstName"
    v-model:last-name="user.lastName"
  />
</template>
```

**Modifiers 접근**

```vue
<script setup lang="ts">
// modifiers 객체 접근
const [model, modifiers] = defineModel<string, 'trim' | 'uppercase'>()

// modifiers 예: { trim: true, uppercase: true }
console.log(modifiers)
</script>
```

> **참고**: `defineModel()`은 이전의 `defineProps` + `defineEmits` 조합을 대체하여 더 간결한 양방향 바인딩을 제공합니다.

### 3.3 컴포저블 및 유틸리티 작성

#### 컴포저블 작성 가이드

컴포저블은 Vue의 Composition API를 활용하여 **재사용 가능한 상태 로직**을 캡슐화합니다.

**예제: 기본 컴포저블**

```typescript
// composables/useCounter.ts
export const useCounter = (initialValue = 0) => {
  const count = ref(initialValue)
  const doubleCount = computed(() => count.value * 2)

  const increment = () => {
    count.value++
  }

  const decrement = () => {
    count.value--
  }

  const reset = () => {
    count.value = initialValue
  }

  return {
    count: readonly(count),
    doubleCount,
    increment,
    decrement,
    reset
  }
}
```

**예제: 비동기 데이터 페칭 컴포저블**

```typescript
// composables/useFetch.ts
export const useFetch = <T>(url: string) => {
  const data = ref<T | null>(null)
  const error = ref<Error | null>(null)
  const loading = ref(false)

  const fetchData = async () => {
    loading.value = true
    error.value = null

    try {
      const response = await $fetch<T>(url)
      data.value = response
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }

  // 자동 실행
  onMounted(() => {
    fetchData()
  })

  return {
    data: readonly(data),
    error: readonly(error),
    loading: readonly(loading),
    refetch: fetchData
  }
}
```

#### 유틸리티 작성 가이드

유틸리티는 **순수 함수**로 작성하며, Vue의 반응성 시스템과 독립적입니다.

**예제: 유틸리티 함수**

```typescript
// utils/format.ts
export const formatCurrency = (value: number, locale = 'ko-KR', currency = 'KRW') => {
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency
  }).format(value)
}

export const formatDate = (date: Date | string, format = 'yyyy-MM-dd') => {
  const d = typeof date === 'string' ? new Date(date) : date
  // 포맷팅 로직
  return d.toISOString().split('T')[0]
}
```

#### 주요 원칙

- 컴포저블: `ref`, `reactive`, `computed` 등으로 상태 구성 후 필요한 값 반환
- **파일명 = 함수명** 일치 권장 (예: `useAuth.ts` → `useAuth()`)
- 유틸리티: **순수 함수** 지향, 부수 효과 회피
- **스토어와 컴포저블 역할 분리**: 컴포저블 내부에서 스토어를 직접 생성/의존하지 않도록 설계
- Nuxt **auto-import** 적극 활용

### 3.4 기타 네이밍 컨벤션

#### 타입 및 인터페이스

**PascalCase**를 사용하며, `I` 접두사는 지양합니다.

```typescript
// ❌ 나쁜 예
interface IUserProfile {
  name: string
  email: string
}

// ✅ 좋은 예
interface UserProfile {
  name: string
  email: string
}

type ApiResponse<T> = {
  data: T
  status: number
  message?: string
}
```

#### 상수 정의

enum 대신 **상수 객체**를 사용하여 타입 안전성과 유연성을 확보합니다.

```typescript
// ❌ enum 사용 지양
enum UserRole {
  ADMIN = 'admin',
  USER = 'user',
  GUEST = 'guest'
}

// ✅ 상수 객체 사용 권장
export const USER_ROLE = {
  ADMIN: 'admin',
  USER: 'user',
  GUEST: 'guest'
} as const

export type UserRole = typeof USER_ROLE[keyof typeof USER_ROLE]
```

---

## 4. 코드 스타일 및 모범 사례

### 4.1 포매팅 및 린팅

**ESLint + Prettier**를 사용하여 일관된 코드 스타일을 유지합니다.

```json
// .eslintrc.json 예시
{
  "extends": [
    "@nuxt/eslint-config",
    "plugin:vue/vue3-recommended",
    "prettier"
  ],
  "rules": {
    "vue/multi-word-component-names": "error",
    "vue/no-v-html": "warn"
  }
}
```

### 4.2 반응성 관리

#### Pinia 스토어 사용 시 주의사항

구조 분해 시 반응성을 잃지 않도록 `storeToRefs()`를 사용합니다.

```typescript
// ❌ 나쁜 예 - 반응성 손실
const userStore = useUserStore()
const { name, email } = userStore // 반응성 손실!

// ✅ 좋은 예 - 반응성 유지
const userStore = useUserStore()
const { name, email } = storeToRefs(userStore)

// 메서드는 직접 구조 분해 가능
const { updateProfile, logout } = userStore
```

#### Reactive 객체 구조 분해

```typescript
// ❌ 나쁜 예
const state = reactive({ count: 0, name: 'John' })
const { count, name } = state // 반응성 손실

// ✅ 좋은 예 1: toRefs 사용
const state = reactive({ count: 0, name: 'John' })
const { count, name } = toRefs(state)

// ✅ 좋은 예 2: 객체 그대로 사용
const state = reactive({ count: 0, name: 'John' })
// 템플릿에서: {{ state.count }}
```

### 4.3 데이터 패칭

#### 서버 사이드 렌더링과 통합

```typescript
// ✅ SSR 지원 - useFetch 사용 (status 포함)
const { data, status, pending, error, refresh } = await useFetch('/api/users')
// status: 'idle' | 'pending' | 'success' | 'error'

// ✅ SSR 지원 - useAsyncData 사용 (signal 옵션 포함 - 권장)
const { data, status, error } = await useAsyncData(
  'users',
  (_nuxtApp, { signal }) => $fetch('/api/users', { signal })
)

// ⚠️ 클라이언트 전용 - $fetch 사용
onMounted(async () => {
  const users = await $fetch('/api/users')
})
```

> **참고**: `signal` 옵션은 요청 취소(abort)를 지원하며, 컴포넌트 언마운트 시 자동으로 요청을 취소합니다.

#### 데이터 패칭 모범 사례

```vue
<script setup lang="ts">
interface User {
  id: number
  name: string
  email: string
}

// 기본 사용
const { data: users, pending, error, refresh } = await useFetch<User[]>('/api/users')

// 조건부 패칭
const userId = ref(1)
const { data: user } = await useFetch(() => `/api/users/${userId.value}`, {
  // userId가 변경되면 자동으로 재요청
  watch: [userId]
})

// 즉시 실행하지 않음
const { data, execute } = await useFetch('/api/users', {
  immediate: false
})

// 버튼 클릭 시 실행
const handleClick = () => {
  execute()
}
</script>
```

#### 병렬 데이터 페칭

여러 API를 동시에 호출할 때는 `Promise.all`과 `signal`을 활용합니다.

```typescript
// 병렬 요청 + signal 지원
const { data: dashboard, status } = await useAsyncData(
  'dashboard',
  async (_nuxtApp, { signal }) => {
    const [users, posts, stats] = await Promise.all([
      $fetch('/api/users', { signal }),
      $fetch('/api/posts', { signal }),
      $fetch('/api/stats', { signal })
    ])
    return { users, posts, stats }
  }
)

// 사용: dashboard.value.users, dashboard.value.posts, dashboard.value.stats
```

### 4.4 컴포넌트 설계 원칙

#### 단일 책임 원칙

각 컴포넌트는 하나의 명확한 책임만 가져야 합니다.

```vue
<!-- ❌ 나쁜 예: 너무 많은 책임 -->
<script setup lang="ts">
// UserDashboard.vue - 사용자 정보, 통계, 설정 등 모든 것을 처리
</script>

<!-- ✅ 좋은 예: 책임 분리 -->
<script setup lang="ts">
// UserDashboard.vue - 레이아웃만 담당
import UserProfile from './UserProfile.vue'
import UserStats from './UserStats.vue'
import UserSettings from './UserSettings.vue'
</script>

<template>
  <div class="dashboard">
    <UserProfile />
    <UserStats />
    <UserSettings />
  </div>
</template>
```

#### Props 전달 규칙

Vue 공식 스타일 가이드에 따라 Props 케이싱을 일관되게 사용합니다.

- **Props 선언**: camelCase 사용
- **템플릿에서 전달**: kebab-case 사용

```vue
<script setup lang="ts">
// Props 선언 시 camelCase
const props = defineProps<{
  greetingMessage: string
  itemCount?: number
}>()
</script>

<template>
  <!-- ✅ 좋은 예: kebab-case로 전달 -->
  <MyComponent greeting-message="hello" :item-count="10" />

  <!-- ❌ 피해야 할 예: camelCase로 전달 -->
  <MyComponent greetingMessage="hello" :itemCount="10" />
</template>
```

#### Scoped 스타일 사용

모든 컴포넌트는 **scoped 스타일** 또는 **CSS Modules**를 사용해야 합니다. 전역 스타일은 `App.vue`와 레이아웃 컴포넌트에서만 허용합니다.

```vue
<style scoped>
/* 컴포넌트에 한정된 스타일 */
.button {
  background-color: #42b883;
}
</style>

<!-- 또는 CSS Modules 사용 -->
<style module>
.button {
  background-color: #42b883;
}
</style>
```

### 4.5 접근성 및 국제화

#### ARIA 속성 사용

```vue
<template>
  <button
    aria-label="메뉴 열기"
    :aria-expanded="isMenuOpen"
    @click="toggleMenu">
    <IconMenu />
  </button>

  <nav :aria-hidden="!isMenuOpen">
    <!-- 메뉴 내용 -->
  </nav>
</template>
```

#### 국제화 (i18n)

```vue
<script setup lang="ts">
const { t } = useI18n()
</script>

<template>
  <div>
    <h1>{{ t('welcome.title') }}</h1>
    <p>{{ t('welcome.description', { name: userName }) }}</p>
  </div>
</template>
```

> **요약 체크리스트**
> - [ ] 폴더/파일명 규칙(PascalCase vs kebab-case) 팀 합의
> - [ ] 베이스 컴포넌트 접두사(`Base|App|V`) 사용
> - [ ] 페이지/레이아웃 단일 루트 요소 보장
> - [ ] 동적 라우트/캐치올/그룹 구조 명확화
> - [ ] 컴포저블 `use*`/스토어 `use*Store` 일관성
> - [ ] ESLint/Prettier 설정 및 CI 체크
> - [ ] Pinia 반응성(`storeToRefs`) 준수

---

## 5. 마무리
본 가이드는 Vue 3 + Nuxt 3 프로젝트의 **구조, 명명, 코드 스타일**을 정리한 실무 템플릿입니다. 팀 합의와 리뷰 문화를 통해 지속적으로 개선하고, 공식 문서/스타일 가이드 업데이트를 주기적으로 반영하세요.