Guides the implementation of Vue 3 Composition API best practices and project structure.
Install
mkdir -p .claude/skills/vue-excatt && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/19452" && unzip -o skill.zip -d .claude/skills/vue-excatt && rm skill.zipInstalls to .claude/skills/vue-excatt
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Vue 3 Composition API 패턴 가이드를 실행합니다.Key capabilities
- →Implement Vue 3 components using script setup syntax
- →Define reactive state with ref and computed properties
- →Create custom composables for reusable logic
- →Manage application state using Pinia stores
- →Configure Vue Router with navigation guards
- →Handle form validation with VeeValidate and Zod
How it works
The skill provides standardized patterns for Vue 3 development, including script setup, composable extraction, and Pinia store definitions. It guides the user through organizing files into specific directories like components, composables, and stores.
Inputs & outputs
When to use vue
- →Implement Vue 3 Composition API
- →Standardize composable functions
- →Structure Vue components
- →Manage state with Pinia
About this skill
Vue Skill
Vue 3 Composition API 패턴 가이드를 실행합니다.
프로젝트 구조
src/
├── main.ts
├── App.vue
├── components/
│ ├── common/
│ └── features/
├── composables/ # Composition 함수
│ ├── useAuth.ts
│ └── useFetch.ts
├── stores/ # Pinia 스토어
│ └── user.ts
├── views/ # 페이지 컴포넌트
├── router/
│ └── index.ts
├── types/
└── utils/
Composition API 기본
Script Setup
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
// Props 정의
const props = defineProps<{
title: string
count?: number
}>()
// Emits 정의
const emit = defineEmits<{
(e: 'update', value: number): void
(e: 'close'): void
}>()
// Reactive State
const count = ref(0)
const user = ref<User | null>(null)
// Computed
const doubleCount = computed(() => count.value * 2)
// Methods
function increment() {
count.value++
emit('update', count.value)
}
// Lifecycle
onMounted(async () => {
user.value = await fetchUser()
})
// Watch
watch(count, (newVal, oldVal) => {
console.log(`Count changed from ${oldVal} to ${newVal}`)
})
</script>
<template>
<div>
<h1>{{ props.title }}</h1>
<p>Count: {{ count }} (Double: {{ doubleCount }})</p>
<button @click="increment">Increment</button>
</div>
</template>
Composables (커스텀 훅)
useFetch
// composables/useFetch.ts
import { ref, shallowRef } from 'vue'
interface UseFetchOptions {
immediate?: boolean
}
export function useFetch<T>(url: string, options: UseFetchOptions = {}) {
const data = shallowRef<T | null>(null)
const error = ref<Error | null>(null)
const isLoading = ref(false)
async function execute() {
isLoading.value = true
error.value = null
try {
const response = await fetch(url)
if (!response.ok) throw new Error('Network error')
data.value = await response.json()
} catch (e) {
error.value = e as Error
} finally {
isLoading.value = false
}
}
if (options.immediate !== false) {
execute()
}
return { data, error, isLoading, execute }
}
// 사용
const { data: users, isLoading, execute: refetch } = useFetch<User[]>('/api/users')
useAuth
// composables/useAuth.ts
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
const user = ref<User | null>(null)
const token = ref<string | null>(localStorage.getItem('token'))
export function useAuth() {
const router = useRouter()
const isAuthenticated = computed(() => !!token.value)
async function login(email: string, password: string) {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
})
const data = await response.json()
token.value = data.token
user.value = data.user
localStorage.setItem('token', data.token)
router.push('/dashboard')
}
function logout() {
token.value = null
user.value = null
localStorage.removeItem('token')
router.push('/login')
}
return { user, token, isAuthenticated, login, logout }
}
Pinia (State Management)
Store 정의
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// State
const users = ref<User[]>([])
const currentUser = ref<User | null>(null)
const isLoading = ref(false)
// Getters
const activeUsers = computed(() =>
users.value.filter(u => u.isActive)
)
const userCount = computed(() => users.value.length)
// Actions
async function fetchUsers() {
isLoading.value = true
try {
const response = await fetch('/api/users')
users.value = await response.json()
} finally {
isLoading.value = false
}
}
async function createUser(data: CreateUserDTO) {
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(data),
})
const newUser = await response.json()
users.value.push(newUser)
return newUser
}
function setCurrentUser(user: User) {
currentUser.value = user
}
// Persist (with pinia-plugin-persistedstate)
return {
users,
currentUser,
isLoading,
activeUsers,
userCount,
fetchUsers,
createUser,
setCurrentUser,
}
}, {
persist: true,
})
// 사용
const userStore = useUserStore()
await userStore.fetchUsers()
Vue Router
설정
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/',
component: () => import('@/views/Home.vue'),
},
{
path: '/users',
component: () => import('@/views/Users.vue'),
meta: { requiresAuth: true },
},
{
path: '/users/:id',
component: () => import('@/views/UserDetail.vue'),
props: true,
},
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/NotFound.vue'),
},
]
const router = createRouter({
history: createWebHistory(),
routes,
})
// Navigation Guard
router.beforeEach((to, from) => {
const { isAuthenticated } = useAuth()
if (to.meta.requiresAuth && !isAuthenticated.value) {
return { path: '/login', query: { redirect: to.fullPath } }
}
})
export default router
useRoute / useRouter
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
import { watch } from 'vue'
const route = useRoute()
const router = useRouter()
// 파라미터 접근
const userId = computed(() => route.params.id as string)
// 쿼리 접근
const page = computed(() => Number(route.query.page) || 1)
// 라우트 변경 감지
watch(() => route.params.id, (newId) => {
fetchUser(newId)
})
// 프로그래매틱 네비게이션
function goToUser(id: string) {
router.push({ name: 'user', params: { id } })
}
</script>
컴포넌트 패턴
v-model 커스텀
<!-- components/CustomInput.vue -->
<script setup lang="ts">
const model = defineModel<string>()
</script>
<template>
<input v-model="model" />
</template>
<!-- 사용 -->
<CustomInput v-model="username" />
Slots
<!-- components/Card.vue -->
<script setup lang="ts">
defineSlots<{
default(): any
header?(): any
footer?(): any
}>()
</script>
<template>
<div class="card">
<div v-if="$slots.header" class="card-header">
<slot name="header" />
</div>
<div class="card-body">
<slot />
</div>
<div v-if="$slots.footer" class="card-footer">
<slot name="footer" />
</div>
</div>
</template>
Provide / Inject
// 부모 컴포넌트
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme)
// 자식 컴포넌트
import { inject } from 'vue'
const theme = inject<Ref<string>>('theme', ref('light'))
Form Handling
VeeValidate + Zod
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { z } from 'zod'
const schema = toTypedSchema(
z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Min 8 characters'),
})
)
const { handleSubmit, errors, defineField, isSubmitting } = useForm({
validationSchema: schema,
})
const [email, emailAttrs] = defineField('email')
const [password, passwordAttrs] = defineField('password')
const onSubmit = handleSubmit(async (values) => {
await login(values.email, values.password)
})
</script>
<template>
<form @submit="onSubmit">
<div>
<input v-model="email" v-bind="emailAttrs" type="email" />
<span v-if="errors.email">{{ errors.email }}</span>
</div>
<div>
<input v-model="password" v-bind="passwordAttrs" type="password" />
<span v-if="errors.password">{{ errors.password }}</span>
</div>
<button type="submit" :disabled="isSubmitting">Login</button>
</form>
</template>
Suspense & 비동기 컴포넌트
<!-- 비동기 컴포넌트 -->
<script setup lang="ts">
const user = await fetchUser() // top-level await
</script>
<!-- 부모에서 Suspense 사용 -->
<template>
<Suspense>
<template #default>
<UserProfile />
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>
체크리스트
- Composition API (script setup)
- TypeScript 타입 정의
- Pinia 상태 관리
- Composables 추출
- Vue Router 설정
- 폼 검증
- 에러 핸들링
출력 형식
## Vue Implementation
### Components
| Component | Props | Emits |
|-----------|-------|-------|
| UserCard | user: User | select |
### Composables
| Hook | Returns | Usage |
|------|---------|-------|
| useAuth | user, login, logout | 인증 |
### Stores
| Store | State | Actions |
|-------|-------|---------|
| user | users[], isLoading | fetchUsers |
요청에 맞는 Vue 3 구현을 설계하세요.
How it compares
Unlike manual implementation, this skill enforces a specific project structure and syntax pattern for Vue 3 Composition API to ensure consistency.
Compared to similar skills
vue side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| vue (this skill) | 0 | 3mo | Review | Intermediate |
| vue-best-practices | 19 | 4mo | No flags | Intermediate |
| vue-testing-best-practices | 5 | 6mo | No flags | Intermediate |
| vue-debug-guides | 4 | 5mo | No flags | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by excatt
View all by excatt →You might also like
vue-best-practices
antfu
MUST be used for Vue.js tasks. Strongly recommends Composition API with `<script setup>` and TypeScript as the standard approach. Covers Vue 3, SSR, Volar, vue-tsc. Load for any Vue, .vue files, Vue Router, Pinia, or Vite with Vue work. ALWAYS use Composition API unless the project explicitly requires Options API.
vue-testing-best-practices
vuejs-ai
Use for Vue.js testing. Covers Vitest, Vue Test Utils, component testing, mocking, testing patterns, and Playwright for E2E testing.
vue-debug-guides
vuejs-ai
Vue 3 debugging and error handling for runtime errors, warnings, async failures, and SSR/hydration issues. Use when diagnosing or fixing Vue issues.
vue-router-best-practices
antfu
Vue Router 4 patterns, navigation guards, route params, and route-component lifecycle interactions.
vueuse
onmax
Use when working with VueUse composables - provides reactive utilities for state, browser APIs, sensors, network, animations. Check VueUse before writing custom composables - most patterns already implemented.
shadcn-vue-admin
Whbbit1999
Build and maintain the shadcn-vue-admin Vue 3 + Vite + TypeScript admin dashboard with shadcn-vue, Tailwind, Pinia, Vue Router, i18n, and TanStack Query. Use for UI/layout changes, page additions, routing updates, theme/auth work, and component integration in this repo.