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.zip

Installs 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 패턴 가이드를 실행합니다.
36 charsno explicit “when” trigger
Intermediate

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

You give it
Vue 3 component requirements or state management needs
You get back
Structured Vue 3 code patterns and implementation tables

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.

SkillInstallsUpdatedSafetyDifficulty
vue (this skill)03moReviewIntermediate
vue-best-practices194moNo flagsIntermediate
vue-testing-best-practices56moNo flagsIntermediate
vue-debug-guides45moNo flagsIntermediate

Try saying

Example prompts that trigger this skill in your AI assistant.

Search skills

Search the agent skills registry