vue-patterns
affaan-m/ECC
Vue, Nuxt, Vite 또는 Pinia 프로젝트를 위한 Vue.js 3 컴포지션 API 패턴, 컴포넌트 아키텍처, 반응성 모범 사례, Pinia 상태 관리, Vue Router 탐색 및 Nuxt SSR 패턴을 제공합니다.
...모든 것을 확장하십시오Vue.js 패턴 및 모범 사례
Composition API를 활용한 Vue.js 3 개발을 위한 종합 가이드()를 활용한 Vue.js 3 개발을 위한 종합 가이드로, 컴포넌트 설계, 반응성, 상태 관리, 라우팅, 테스트 및 SSR 패턴을 다룹니다. Nuxt가 순수 Vue와 다른 부분에서는 Nuxt 전용 지침이 포함되어 있습니다.
활성화 시점
다음과 같은 경우 이 스킬을 활성화하세요:
- 프로젝트에서 Vue.js(모든 버전), Nuxt, Vite + Vue 또는 Pinia를 사용하는 경우.
- 사용자가 Vue 컴포넌트 아키텍처, 컴포저블, 반응성 또는 상태 관리에 대해 질문할 때.
- Vue 단일 파일 컴포넌트(
.vue파일)를 검토할 때. - Vue Router, Pinia 스토어 또는 Vite/Vitest 구성을 설정할 때.
- Vue 특유의 성능, 보안 또는 SSR 패턴에 대해 논의할 때.
1. 프로젝트 구조
권장 레이아웃 (기능 우선)
src/
├── api/ # API 클라이언트 및 엔드포인트 정의
├── assets/ # 정적 자산 (이미지, 폰트, 아이콘)
├── components/ # 공유/재사용 가능한 컴포넌트
│ ├── base/ # 기본 UI 구성 요소 (Button, Input, Modal)
│ └── features/ # 기능별 공유 컴포넌트
├── composables/ # 재사용 가능한 Composition API 로직
├── layouts/ # 페이지 레이아웃 (선택 사항)
├── pages/ # 라우트 수준 페이지 컴포넌트
├── router/ # Vue Router 구성
├── stores/ # Pinia 스토어
├── types/ # TypeScript 타입 정의
├── utils/ # 순수 유틸리티 함수
└── App.vue # 루트 컴포넌트
파일 명명 규칙
| 규칙 | 사용 시점 |
|---|---|
PascalCase.vue |
모든 컴포넌트 ( vue/multi-word-component-names에 의해 강제 적용됨) |
useCamelCase.ts |
컴포저블 |
camelCase.ts |
유틸리티, API 클라이언트, 타입 |
케밥 케이스 디렉터리 |
경로 세그먼트, 기능 폴더 |
2. 컴포넌트 아키텍처
단일 파일 컴포넌트 순서
표현형 컴포넌트 대 컨테이너형 컴포넌트
- 컨테이너 컴포넌트: 데이터 가져오기, 상태 및 부수 효과를 직접 관리합니다. 프리젠테이션 컴포넌트를 렌더링합니다.
- 표현형 컴포넌트: 프로프(props)를 수신하고 이벤트를 발생시킵니다. API 호출이나 스토어 접근이 없습니다. 순수한 렌더링만 수행합니다.
프로프 모범 사례
// 기본값이 지정된 타입 기반 props
interface Props {
label: string;
variant?: "primary" | "secondary";
disabled?: boolean;
items: Item[];
}
const props = withDefaults(defineProps(), {
variant: "primary",
disabled: false,
});
- 항상
타입을명시하고, 필요한 경우필수 속성및기본값을지정하세요. - 부울형 프로퍼티:
isXxx,hasXxx,canXxx. - 프로퍼티를 절대 직접 수정하지 말고, 대신 이벤트를 발생시키세요.
- v-model 바인딩의 경우,
defineModel()(Vue 3.4 이상) 또는modelValue+update:modelValue를사용하세요.
이벤트
const emit = defineEmits<{
submit: [];
"update:modelValue": [value: string];
select: [id: string, index: number];
}>();
- 템플릿에서는 kebab-case를 사용하세요 (
@update:model-value). - 스크립트에서는 camelCase를 사용하세요 (
emit("update:modelValue", val)).
3. 컴포저블(재사용 가능한 로직)
구조
// composables/useDebounce.ts
export function useDebounce(value: MaybeRef, delay: number): Ref {
const debounced = ref(toValue(value)) as Ref;
let timer: ReturnType;
watch(
() => toValue(value),
(newVal) => {
clearTimeout(timer);
timer = setTimeout(() => { debounced.value = newVal; }, delay);
}
);
onUnmounted(() => clearTimeout(timer));
return readonly(debounced);
}
규칙
- 반드시
'use'접두사로 시작해야 합니다. - 반응형 값(
ref,computed,reactive)을 반환해야 하며, 절대 일반 기본형(primitives)을 반환해서는 안 됩니다. MaybeRef/toRef()/toValue()를 통해 반응형 입력을 받아들여야 합니다.onUnmounted또는 watcher의onCleanup에서 부수 효과를 정리해야 합니다.- 모듈 범위 내의 부수 효과는 허용되지 않습니다.
vs 믹신
Composables는 Vue 2의 믹신을 완전히 대체합니다:
- 믹신: 불투명한 데이터 흐름, 단일 진실원 충돌, 이름 충돌.
- 컴포저블: 명시적인 임포트, 명확한 반환 값, 조합 가능하며 트리 셰이킹이 가능합니다.
4. 상태 관리
언제 무엇을 사용할지
| 패턴 | 사용 사례 |
|---|---|
ref() / reactive() |
로컬 컴포넌트 상태 |
| 프로프 + 에미트 | 부모-자식 간 통신 |
| 제공 / 주입 | 테마, 구성, 플러그인 API |
| Pinia 스토어 | 전역, 공유, 복합 상태 |
| 서버 상태 컴포저블 | 캐싱 기능이 포함된 API 데이터 ( fetch/TanStack Query 래핑) |
Pinia 스토어 설정 (권장)
// stores/useCartStore.ts
export const useCartStore = defineStore("cart", () => {
const items = ref([]);
const isLoading = ref(false);
const totalPrice = computed(() =>
items.value.reduce((sum, i) => sum + i.price * i.quantity, 0)
);
const itemCount = computed(() =>
items.value.reduce((sum, i) => sum + i.quantity, 0)
);
async function addItem(productId: string) {
isLoading.value = true;
try {
const item = await fetchProduct(productId);
const existing = items.value.find(i => i.id === item.id);
if (existing) existing.quantity++;
else items.value.push({ ...item, quantity: 1 });
} finally {
isLoading.value = false;
}
}
return { items, isLoading, totalPrice, itemCount, addItem };
});
- Setup Store 구문(Options Store가 아님)을 사용하세요.
- 비즈니스 수준의 변경에는 액션을, 그룹화된 업데이트에는
$patch()를 우선적으로 사용하세요. - 모든 비동기 액션: 로딩 + 성공 + 오류 처리.
5. Vue Router
경로 정의
const routes = [
{
path: "/users/:id",
name: "user-detail",
component: () => import("@/pages/UserDetail.vue"), // 지연 로딩
props: true, // 매개변수를 props로 전달
meta: { requiresAuth: true },
},
];
네비게이션 가드
router.beforeEach((to, from) => {
const { isLoggedIn } = useAuthStore();
if (to.meta.requiresAuth && !isLoggedIn) {
return { name: "login", query: { redirect: to.fullPath } };
}
});
반응형 라우트 매개변수
컴포넌트가 마운트된 상태에서 라우트 매개변수가 변경될 때:
const route = useRoute();
const id = computed(() => route.params.id as string);
watch(id, (newId) => fetchItem(newId));
6. 템플릿 패턴
템플릿 구문
로딩 중...
오류: {{ error }}
{{ content }}
토글된 콘텐츠
{{ item.name }}
{{ item.name }}
7. 성능
| 기법 | 사용 시점 |
|---|---|
v-memo |
변경 빈도가 낮은 목록 항목 |
v-once |
한 번 렌더링된 후 영구적으로 고정되는 콘텐츠 |
shallowRef() |
대규모 데이터 구조를 일괄적으로 교체 |
shallowReactive() |
최상위 속성만 반응형입니다 |
v-if 대신v-show |
빈번한 표시/숨기기 전환 |
|
토글된 뷰 캐싱 |
| 지연 로드 경로 | 중요하지 않은 경로의 경우() => import(...) |
Suspense |
폴백 기능을 갖춘 비동기 컴포넌트 로딩 |
8. 테스트
Stack
- 단위 테스트 및 컴포넌트 테스트를 위한Vitest
- 마운팅 및 상호작용을 위한Vue Test Utils
- 스토어 모킹을 위한@pinia/testing
- E2E 테스트용Playwright
컴포넌트 테스트 패턴
import { mount } from "@vue/test-utils";
import { createPinia, setActivePinia } from "pinia";
import UserCard from "./UserCard.vue";
beforeEach(() => { setActivePinia(createPinia()); });
it("렌더링되고 이벤트가 발생합니다", async () => {
const wrapper = mount(UserCard, {
props: { user: { id: "1", name: "Alice" } },
});
expect(wrapper.text()).toContain("Alice");
await wrapper.find("button").trigger("click");
expect(wrapper.emitted("select")![0]).toEqual(["1"]);
});
9. Nuxt 전용 패턴
자동 임포트
Nuxt는 ref, computed, watch, useFetch, useAsyncData 등을 자동으로 임포트합니다. 임포트 선언 없이 바로 사용하세요. Nuxt가 아닌 프로젝트에서는 항상 명시적으로 임포트해야 합니다.
useAsyncData / useFetch
const { data: user, pending, error, refresh } = await useAsyncData(
"user", // 캐싱을 위한 고유 키
() => $fetch(`/api/users/${id}`),
);
const { data: posts } = await useFetch("/api/posts", {
query: { page: 1 },
key: "posts-page-1", // 중복 요청 방지
});
서버 라우트
// server/api/users/[id].ts
export default defineEventHandler(async (event) => {
const { id } = await getValidatedRouterParams(event, z.object({
id: z.string().uuid(),
}).parse);
// ... 데이터를 가져와 반환
});
런타임 구성
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// 서버 전용
apiSecret: "",
// 공개 (클라이언트에 노출)
public: {
apiBase: "https://api.example.com",
},
},
});
10. Vue 3.5+의 새로운 API
반응형 프로프스 해체
Vue 3.5에서는 반응형 프로프스 해체 기능이 안정화되었습니다. defineProps() 에서 해체된 변수는 자동으로 반응형이 됩니다:
// Vue 3.5 이상: 분해된 프로프스는 반응형입니다 (toRefs가 필요 없음)
const { count = 0, msg = "hello" } = defineProps<{
count?: number;
msg?: string;
}>();
// 제한 사항: 분해 할당된 프로퍼티를 직접 감시할 수 없음
watch(() => count, (newVal) => { ... }); // 통과, getter 필수
useTemplateRef()
템플릿 참조의 경우, 이름이 일치하는 일반 ref를 useTemplateRef() 로 대체하세요:
import { useTemplateRef } from "vue";
const inputEl = useTemplateRef("input");
// "input"은 변수 이름이 아닌 템플릿 내의 ref="input" 속성과 일치합니다
동적 ref ID 지원: useTemplateRef(dynamicRefId).
onWatcherCleanup()
전역에서 임포트 가능한 워처 정리 API(Vue 3.5 이상). 워처 콜백 내부에서 동기식으로 호출해야 합니다:
import { watch, onWatcherCleanup } from "vue";
watch(userId, async (newId) => {
const controller = new AbortController();
onWatcherCleanup(() => controller.abort());
// ... 신호를 사용하여 데이터 가져오기
});
useId()
폼 요소 및 접근성을 위한 SSR 호환 고유 ID 생성:
import { useId } from "vue";
const id = useId();
defer Teleport
동일한 렌더링 사이클 내에서 렌더링된 대상 위치로 텔레포트할 수 있게 합니다:
콘텐츠
지연 하이드레이션 (SSR)
defineAsyncComponent() 가 이제 하이드레이션 전략을 지원합니다:
import { defineAsyncComponent, hydrateOnVisible } from "vue";
const AsyncComp = defineAsyncComponent({
loader: () => import("./Comp.vue"),
hydrate: hydrateOnVisible(),
});
안티패턴
| 안티패턴 | 왜 잘못된가 | 해결 방법 |
|---|---|---|
defineProps() 의 디스트럭처링 (Vue 3.5 미만) |
스냅샷을 캡처하여 반응성을 상실함 | props.xxx를 통해 접근하거나 toRefs() 사용 |
해체된 prop에watch() 적용 (Vue 3.5+) |
컴파일 시간 오류 — 구조 해체된 props는 직접 감시할 수 없음 | 게터 래퍼 사용: watch(() => count, ...) |
동일한 요소에v-if + v-for 적용 |
모호한 실행 순서 | 계산된 필터링된 배열 사용 |
v-for 키 = 인덱스 |
순서 변경 시 상태 오류 | 안정적인 데이터베이스 ID 사용 |
| 프로퍼티 변경 | 단방향 데이터 흐름 위반 | 이벤트 발생 또는 v-model 사용 |
사용자 콘텐츠가 포함된v-html |
XSS 취약점 | DOMPurify를 사용하여 정제 |
| Vue 3의 믹스인 | 불투명하고 충돌 발생 가능성이 높음 | 컴포저블로 대체 |
| 컴포저블 내 모듈 범위 부작용 | 인스턴스 간 공유 | onMounted 및 onUnmounted 내의 스코프 |
교체 가능한 상태를 위한reactive() |
교체 시 반응성이 깨짐 | 대신 ref() 사용 |
| 정리 기능이 없는 Watcher | 메모리 누수, 경합 상태 | onCleanup 또는 onWatcherCleanup() 사용 (Vue 3.5 이상) |
| 새로운 Vue 3 코드의 Options API | 생태계가 Composition API로 전환 중 | 사용법 |
| 템플릿 참조용 일반 ref | 동적 ref 미지원, 이름 일치에 취약 | useTemplateRef() 사용 (Vue 3.5+) |
관련 기술
접근성— ARIA, 의미론적 HTML, 포커스 관리프론트엔드 패턴— 크로스 프레임워크 프론트엔드 아키텍처TypeScript— Vue 프로젝트에 적용된 TypeScript 모범 사례코딩 표준— 일반적인 코드 품질 표준
---
name: vue-patterns
description: Provides Vue.js 3 Composition API patterns, component architecture, reactivity best practices, Pinia state management, Vue Router navigation, and Nuxt SSR patterns for Vue, Nuxt, Vite, or Pinia projects.
---
# Vue.js Patterns and Best Practices
Comprehensive guide for Vue.js 3 development using Composition API (`<script setup>`), covering component design, reactivity, state management, routing, testing, and SSR patterns. Nuxt-specific guidance is included where it differs from vanilla Vue.
## When to Activate
Activate this skill when:
- The project uses Vue.js (any version), Nuxt, Vite + Vue, or Pinia.
- The user asks about Vue component architecture, composables, reactivity, or state management.
- Reviewing Vue Single-File Components (`.vue` files).
- Setting up Vue Router, Pinia stores, or Vite/Vitest configuration.
- Discussing Vue-specific performance, security, or SSR patterns.
---
## 1. Project Structure
### Recommended Layout (Feature-First)
```
src/
├── api/ # API client and endpoint definitions
├── assets/ # Static assets (images, fonts, icons)
├── components/ # Shared/reusable components
│ ├── base/ # Base UI primitives (Button, Input, Modal)
│ └── features/ # Feature-specific shared components
├── composables/ # Reusable Composition API logic
├── layouts/ # Page layouts (optional)
├── pages/ # Route-level page components
├── router/ # Vue Router configuration
├── stores/ # Pinia stores
├── types/ # TypeScript type definitions
├── utils/ # Pure utility functions
└── App.vue # Root component
```
### File Naming
| Convention | When to Use |
|-----------|-------------|
| `PascalCase.vue` | All components (enforced by `vue/multi-word-component-names`) |
| `useCamelCase.ts` | Composables |
| `camelCase.ts` | Utilities, API clients, types |
| `kebab-case` directories | Route segments, feature folders |
---
## 2. Component Architecture
### Single-File Component Order
```vue
<script setup lang="ts">
// 1. Imports (vue → ecosystem → absolute → relative)
// 2. Props & Emits & Slots
// 3. Composables
// 4. Local state (ref/reactive)
// 5. Computed properties
// 6. Methods
// 7. Watchers
// 8. Lifecycle hooks
</script>
<template>
<!-- Template content -->
</template>
<style scoped>
/* Scoped styles */
</style>
```
### Presentational vs Container
- **Container components**: Own data fetching, state, and side effects. Render presentational components.
- **Presentational components**: Receive props, emit events. No API calls, no store access. Pure rendering.
### Props Best Practices
```ts
// Type-based props with defaults
interface Props {
label: string;
variant?: "primary" | "secondary";
disabled?: boolean;
items: Item[];
}
const props = withDefaults(defineProps<Props>(), {
variant: "primary",
disabled: false,
});
```
- Always provide `type`, and `required`/`default` where appropriate.
- Boolean props: `isXxx`, `hasXxx`, `canXxx`.
- Never mutate props — emit events instead.
- For v-model binding, use `defineModel()` (Vue 3.4+) or `modelValue` + `update:modelValue`.
### Events
```ts
const emit = defineEmits<{
submit: [];
"update:modelValue": [value: string];
select: [id: string, index: number];
}>();
```
- Use kebab-case in templates (`@update:model-value`).
- Use camelCase in script (`emit("update:modelValue", val)`).
---
## 3. Composables (Reusable Logic)
### Structure
```ts
// composables/useDebounce.ts
export function useDebounce<T>(value: MaybeRef<T>, delay: number): Ref<T> {
const debounced = ref(toValue(value)) as Ref<T>;
let timer: ReturnType<typeof setTimeout>;
watch(
() => toValue(value),
(newVal) => {
clearTimeout(timer);
timer = setTimeout(() => { debounced.value = newVal; }, delay);
}
);
onUnmounted(() => clearTimeout(timer));
return readonly(debounced);
}
```
### Rules
- Must start with `use` prefix.
- Return reactive values (`ref`, `computed`, `reactive`), never plain primitives.
- Accept reactive inputs via `MaybeRef` / `toRef()` / `toValue()`.
- Clean up side effects in `onUnmounted` or watcher `onCleanup`.
- No module-scope side effects.
### vs Mixins
Composables replace Vue 2 mixins entirely:
- **Mixins**: Opaque data flow, source-of-truth collisions, name conflicts.
- **Composables**: Explicit imports, clear return values, composable and tree-shakable.
---
## 4. State Management
### When to Use What
| Pattern | Use Case |
|---------|----------|
| `ref()` / `reactive()` | Local component state |
| Props + Emits | Parent-child communication |
| Provide / Inject | Theme, config, plugin API |
| Pinia store | Global, shared, complex state |
| Server state composable | API data with caching (wrap `fetch`/TanStack Query) |
### Pinia Setup Store (Preferred)
```ts
// stores/useCartStore.ts
export const useCartStore = defineStore("cart", () => {
const items = ref<CartItem[]>([]);
const isLoading = ref(false);
const totalPrice = computed(() =>
items.value.reduce((sum, i) => sum + i.price * i.quantity, 0)
);
const itemCount = computed(() =>
items.value.reduce((sum, i) => sum + i.quantity, 0)
);
async function addItem(productId: string) {
isLoading.value = true;
try {
const item = await fetchProduct(productId);
const existing = items.value.find(i => i.id === item.id);
if (existing) existing.quantity++;
else items.value.push({ ...item, quantity: 1 });
} finally {
isLoading.value = false;
}
}
return { items, isLoading, totalPrice, itemCount, addItem };
});
```
- Use Setup Store syntax (not Options Store).
- Prefer actions for business-level mutations and `$patch()` for grouped updates.
- Every async action: handle loading + success + error.
---
## 5. Vue Router
### Route Definitions
```ts
const routes = [
{
path: "/users/:id",
name: "user-detail",
component: () => import("@/pages/UserDetail.vue"), // lazy
props: true, // pass params as props
meta: { requiresAuth: true },
},
];
```
### Navigation Guards
```ts
router.beforeEach((to, from) => {
const { isLoggedIn } = useAuthStore();
if (to.meta.requiresAuth && !isLoggedIn) {
return { name: "login", query: { redirect: to.fullPath } };
}
});
```
### Reactive Route Params
When a component stays mounted but route params change:
```ts
const route = useRoute();
const id = computed(() => route.params.id as string);
watch(id, (newId) => fetchItem(newId));
```
---
## 6. Template Patterns
### Template Syntax
```vue
<!-- v-if/v-else-if/v-else -->
<div v-if="isLoading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<div v-else>{{ content }}</div>
<!-- v-show for frequent toggles -->
<div v-show="isOpen">Toggled content</div>
<!-- v-for with stable keys -->
<div v-for="item in items" :key="item.id">{{ item.name }}</div>
<!-- Computed filtered list (not v-if + v-for on same element) -->
<div v-for="item in activeItems" :key="item.id">{{ item.name }}</div>
<!-- Event handling -->
<form @submit.prevent="handleSubmit">
<button type="submit">Save</button>
</form>
<!-- v-model -->
<input v-model="name" />
<CustomInput v-model="value" v-model:title="title" />
```
---
## 7. Performance
| Technique | When to Use |
|-----------|-------------|
| `v-memo` | List items that rarely change |
| `v-once` | Content rendered once and static forever |
| `shallowRef()` | Large data structures replaced wholesale |
| `shallowReactive()` | Only top-level properties are reactive |
| `v-show` over `v-if` | Frequent visibility toggles |
| `<KeepAlive :max="10">` | Cache toggled views |
| Lazy routes | `() => import(...)` for non-critical routes |
| `Suspense` | Async component loading with fallback |
---
## 8. Testing
### Stack
- **Vitest** for unit and component tests
- **Vue Test Utils** for mounting and interaction
- **@pinia/testing** for store mocking
- **Playwright** for E2E
### Component Test Pattern
```ts
import { mount } from "@vue/test-utils";
import { createPinia, setActivePinia } from "pinia";
import UserCard from "./UserCard.vue";
beforeEach(() => { setActivePinia(createPinia()); });
it("renders and emits", async () => {
const wrapper = mount(UserCard, {
props: { user: { id: "1", name: "Alice" } },
});
expect(wrapper.text()).toContain("Alice");
await wrapper.find("button").trigger("click");
expect(wrapper.emitted("select")![0]).toEqual(["1"]);
});
```
---
## 9. Nuxt-Specific Patterns
### Auto-Imports
Nuxt auto-imports `ref`, `computed`, `watch`, `useFetch`, `useAsyncData`, etc. Use them directly without importing. For non-Nuxt projects, always import explicitly.
### useAsyncData / useFetch
```ts
const { data: user, pending, error, refresh } = await useAsyncData(
"user", // unique key for caching
() => $fetch(`/api/users/${id}`),
);
const { data: posts } = await useFetch("/api/posts", {
query: { page: 1 },
key: "posts-page-1", // dedupes requests
});
```
### Server Routes
```ts
// server/api/users/[id].ts
export default defineEventHandler(async (event) => {
const { id } = await getValidatedRouterParams(event, z.object({
id: z.string().uuid(),
}).parse);
// ... fetch and return
});
```
### Runtime Config
```ts
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// server-only
apiSecret: "",
// public (exposed to client)
public: {
apiBase: "https://api.example.com",
},
},
});
```
---
## 10. Vue 3.5+ New APIs
### Reactive Props Destructure
Vue 3.5 stabilized reactive props destructure — destructured variables from `defineProps()` are automatically reactive:
```ts
// Vue 3.5+: destructured props are reactive (no need for toRefs)
const { count = 0, msg = "hello" } = defineProps<{
count?: number;
msg?: string;
}>();
// Limitation: cannot watch destructured prop directly
watch(() => count, (newVal) => { ... }); // PASS getter required
```
### `useTemplateRef()`
Replace name-matched plain refs with `useTemplateRef()` for template references:
```ts
import { useTemplateRef } from "vue";
const inputEl = useTemplateRef<HTMLInputElement>("input");
// "input" matches the ref="input" attribute in template, not the variable name
```
Supports dynamic ref IDs: `useTemplateRef(dynamicRefId)`.
### `onWatcherCleanup()`
Globally importable watcher cleanup API (Vue 3.5+). It must be called synchronously inside the watcher callback:
```ts
import { watch, onWatcherCleanup } from "vue";
watch(userId, async (newId) => {
const controller = new AbortController();
onWatcherCleanup(() => controller.abort());
// ... fetch with signal
});
```
### `useId()`
SSR-stable unique ID generation for form elements and accessibility:
```ts
import { useId } from "vue";
const id = useId();
```
### `defer` Teleport
`<Teleport defer>` allows teleporting to targets rendered in the same cycle:
```vue
<Teleport defer to="#container">Content</Teleport>
<div id="container"></div>
```
### Lazy Hydration (SSR)
`defineAsyncComponent()` now supports `hydrate` strategy:
```ts
import { defineAsyncComponent, hydrateOnVisible } from "vue";
const AsyncComp = defineAsyncComponent({
loader: () => import("./Comp.vue"),
hydrate: hydrateOnVisible(),
});
```
---
## Anti-Patterns
| Anti-Pattern | Why It's Wrong | The Fix |
|-------------|---------------|---------|
| Destructuring `defineProps()` (Vue < 3.5) | Captures snapshot, loses reactivity | Access via `props.xxx` or use `toRefs()` |
| `watch()` on destructured prop (Vue 3.5+) | Compile-time error — destructured props can't be watched directly | Use getter wrapper: `watch(() => count, ...)` |
| `v-if` + `v-for` on same element | Ambiguous execution order | Use computed filtered array |
| `v-for` key = index | Broken state on reorder | Use stable database IDs |
| Mutating props | Violates one-way data flow | Emit events or use `v-model` |
| `v-html` with user content | XSS vulnerability | Sanitize with DOMPurify |
| Mixins in Vue 3 | Opaque, collision-prone | Replace with composables |
| Module-scope side effects in composable | Shared across instances | Scope in `onMounted` + `onUnmounted` |
| `reactive()` for replaceable state | Replacement breaks reactivity | Use `ref()` instead |
| Watcher without cleanup | Memory leaks, race conditions | Use `onCleanup` or `onWatcherCleanup()` (Vue 3.5+) |
| Options API in new Vue 3 code | Ecosystem move to Composition API | Use `<script setup>` |
| Plain ref for template references | No dynamic ref support, name-matching fragile | Use `useTemplateRef()` (Vue 3.5+) |
## Related Skills
- `accessibility` — ARIA, semantic HTML, focus management
- `frontend-patterns` — Cross-framework frontend architecture
- `typescript` — TypeScript best practices applied to Vue projects
- `coding-standards` — General code quality standards
모든 파일
1개 파일vue-patterns 설치
스킬 파일을 다운로드한 후 .claude/skills/ 디렉터리에 압축을 풀어주세요.
ZIP 다운로드저장소를 클론하고 스킬 파일을 프로젝트에 복사하세요.
git clone https://github.com/affaan-m/ECC/tree/main/skills/vue-patterns # Copy SKILL.md to your .claude/skills/ directory
복사





집
