AI Skills로 Vue 개발하기 (2) - 컴포넌트 설계와 Composition API
목차 보기
시리즈 목차 보기2 / 6
AI Skills로 Vue 개발하기
2 / 6
이전 편 요약
1편에서는 Antfu 컨벤션 기반의 ESLint flat config 설정과 pnpm 카탈로그를 다뤘다. 프로젝트의 뼈대를 잡는 작업이었다면, 이번 편에서는 실제 컴포넌트를 설계하면서 Vue 스킬이 어떻게 도움을 주는지 살펴본다.
Vue 스킬의 구조
vue-skills는 Progressive Disclosure 패턴으로 구성되어 있다. SKILL.md(얇은 진입점)와 references/(세부 가이드)로 나뉜다.
skills/vue/
├── SKILL.md # 진입점 (항상 로드)
└── references/
├── pinia-guide.md # Pinia 상태 관리
├── router-guide.md # Vue Router
├── testing-guide.md # 테스팅 전략
├── vueuse-guide.md # VueUse 유틸리티
└── ...
"Pinia store 만들어줘"라고 요청하면 AI가 pinia-guide.md -> pinia/core-stores.md 순서로 단계적으로 읽는다. 100개 이상의 best practices 가이드가 포함되어 있어서 deep watch 남용, reactive 구조 분해, computed side effect 같은 흔한 함정을 자동으로 피해 간다.
script setup과 TypeScript
<script setup lang="ts">는 Vue 3의 사실상 표준이다. 스킬이 특히 빛을 발하는 건 Generic 컴포넌트를 작성할 때다. defineProps와 defineEmits의 타입 지정 방식이 직관적이면서도 헷갈리는데, 스킬이 매번 정확한 패턴을 제시해준다.
<script setup lang="ts" generic="T extends Record<string, unknown>">
interface Props {
items: T[]
columns: Array<{
key: keyof T
label: string
sortable?: boolean
}>
}
const props = defineProps<Props>()
const emit = defineEmits<{
select: [item: T]
sort: [key: keyof T, direction: 'asc' | 'desc']
}>()
</script>generic 속성으로 타입 파라미터를 선언하고, defineEmits에 tuple 형태로 페이로드 타입을 정의하면 호출부에서 자동 완성까지 된다. 테이블이나 리스트 같은 범용 컴포넌트에서 반복적으로 쓰이는 패턴이다.
Composable 설계 원칙
Composable은 Vue 3에서 로직을 재사용하는 핵심 수단이다. 스킬에서 강조하는 원칙은 세 가지다. use 접두사 컨벤션, 반환값의 readonly 보호, 그리고 VueUse(useLocalStorage, useDebounceFn 등)로 바퀴를 다시 만들지 않는 것이다.
아래는 테이블 정렬 composable 예시다.
import { computed, readonly, ref } from 'vue'
import type { Ref } from 'vue'
interface UseSortOptions<T> {
items: Ref<T[]>
defaultKey?: keyof T
}
export function useTableSort<T extends Record<string, unknown>>({
items,
defaultKey,
}: UseSortOptions<T>) {
const sortKey = ref<keyof T | null>(defaultKey ?? null)
const sortDirection = ref<'asc' | 'desc'>('asc')
const sorted = computed(() => {
if (!sortKey.value) return items.value
return [...items.value].sort((a, b) => {
const val = a[sortKey.value!] > b[sortKey.value!] ? 1 : -1
return sortDirection.value === 'asc' ? val : -val
})
})
function toggleSort(key: keyof T): void {
if (sortKey.value === key) {
sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc'
} else {
sortKey.value = key as any
sortDirection.value = 'asc'
}
}
return {
sortKey: readonly(sortKey),
sortDirection: readonly(sortDirection),
sorted,
toggleSort,
}
}items를 Ref로 받아 반응성을 유지하고, 외부에는 readonly로 감싼 상태와 토글 함수만 노출한다. composable 설계의 기본 골격이다.
vue-code-optimizer 에이전트
vue-code-optimizer는 직접 만든 커스텀 서브에이전트다. Claude Code는 subagent_type을 설정 파일에 등록하면 특정 작업에 특화된 에이전트를 만들 수 있는데, Vue 코드를 작성한 뒤 자동으로 리뷰를 돌려서 개선점을 제안하도록 구성했다. 실제로 겪은 사례들이다.
- 대량 데이터를 다루는
ref에 대해shallowRef사용 권장 computed내부의 side effect 제거 요청watchEffect에서 cleanup 함수 누락 감지
처음에는 사소하게 느껴졌는데, shallowRef 적용만으로도 대량 데이터 테이블의 렌더링 성능이 체감될 정도로 개선됐다.
Before/After 비교
vue-code-optimizer의 리뷰 전후를 비교하면 차이가 명확하다.
개선 전 코드다.
<script setup lang="ts">
const items = ref<Item[]>([])
watch(items, (val) => {
console.log(val)
}, { deep: true })
</script>ref + deep: true 조합은 배열 내부의 모든 변경을 감시하므로 성능에 불리하고, cleanup이 없어서 언마운트 후에도 비동기 작업이 남을 수 있다. 개선 후 코드다.
<script setup lang="ts">
const items = shallowRef<Item[]>([])
watchEffect((onCleanup) => {
const controller = new AbortController()
fetchItems(controller.signal).then(data => items.value = data)
onCleanup(() => controller.abort())
})
</script>shallowRef는 참조가 바뀔 때만 트리거되고, watchEffect의 onCleanup으로 이전 요청을 abort 처리하면 race condition도 방지된다. 이런 패턴을 매번 기억하기는 어려운데, optimizer가 자동으로 잡아주니 리뷰 비용이 크게 줄었다.
정리
Vue 스킬의 Progressive Disclosure 구조, Generic 컴포넌트와 Composable 설계 패턴, 그리고 vue-code-optimizer의 자동 리뷰까지 살펴봤다. 스킬이 제시하는 패턴을 따르면 Vue 특유의 반응성 함정을 대부분 피할 수 있다.
다음 편에서는 Pinia 상태 관리와 Vue Router를 다룬다.