Vue 컴포넌트 작성 패턴 가이드
이 문서는 프로젝트에서 사용하는 컴포넌트 작성 패턴을 설명합니다.
핵심 라이브러리
| 라이브러리 | 용도 | 문서 |
|---|---|---|
| Reka UI | 헤드리스 UI 컴포넌트 | Primitive |
| CVA | 스타일 Variants 관리 | Getting Started |
| shadcn-vue | UI 컴포넌트 참조 | GitHub (v4) |
컴포넌트 작성 패턴
1. CVA (Class Variance Authority) 패턴
CVA를 사용하여 컴포넌트의 다양한 스타일 변형을 관리합니다.
vue
<script setup lang="ts">
import type { VariantProps } from 'class-variance-authority'
import { cva } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
// 기본 스타일
'inline-flex items-center justify-center rounded-md font-medium transition-colors',
{
variants: {
variant: {
default: 'bg-primary text-white hover:bg-primary/90',
outline: 'border border-input bg-background hover:bg-accent',
ghost: 'hover:bg-accent hover:text-accent-foreground',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 px-3',
lg: 'h-11 px-8',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
)
type ButtonVariants = VariantProps<typeof buttonVariants>
interface Props {
variant?: ButtonVariants['variant']
size?: ButtonVariants['size']
class?: string
}
const props = withDefaults(defineProps<Props>(), {
variant: 'default',
size: 'default',
})
</script>
<template>
<button :class="cn(buttonVariants({ variant, size }), props.class)">
<slot />
</button>
</template>2. Reka UI Primitive 패턴
Primitive를 사용하여 렌더링되는 HTML 요소를 동적으로 변경합니다.
vue
<script setup lang="ts">
import { Primitive, type PrimitiveProps } from 'reka-ui'
interface Props extends PrimitiveProps {
class?: string
}
const props = withDefaults(defineProps<Props>(), {
as: 'div',
})
</script>
<template>
<Primitive :as="props.as" :class="props.class">
<slot />
</Primitive>
</template>3. CVA + Primitive 조합 패턴
두 패턴을 조합하여 유연하고 타입 안전한 컴포넌트를 만듭니다.
vue
<script setup lang="ts">
import type { VariantProps } from 'class-variance-authority'
import { cva } from 'class-variance-authority'
import { Primitive } from 'reka-ui'
import { cn } from '@/lib/utils'
const cardVariants = cva('rounded-lg border', {
variants: {
variant: {
default: 'bg-card text-card-foreground',
elevated: 'bg-card shadow-lg',
},
},
defaultVariants: {
variant: 'default',
},
})
type CardVariants = VariantProps<typeof cardVariants>
interface Props {
variant?: CardVariants['variant']
class?: string
}
const props = defineProps<Props>()
</script>
<template>
<Primitive
as="div"
:class="cn(cardVariants({ variant: props.variant }), props.class)"
>
<slot />
</Primitive>
</template>유틸리티 함수
cn() 함수
clsx와 tailwind-merge를 조합한 유틸리티 함수입니다.
typescript
// lib/utils.ts
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}Tailwind v4 주의사항
JIT 클래스 감지 문제
Tailwind v4 JIT는 소스 코드를 정적으로 스캔합니다. 동적 문자열 조합은 감지하지 못합니다.
typescript
// ❌ JIT가 감지하지 못함
const sizes = {
sm: 'px-2.5 py-1',
md: 'px-3 py-1.5', // py-1.5가 생성되지 않음
}
// ✅ CVA를 사용하면 정적 분석 가능
const buttonVariants = cva('...', {
variants: {
size: {
sm: 'px-2.5 py-1',
md: 'px-3 py-1.5', // 정상 작동
},
},
})폴백 유틸리티
JIT가 감지하지 못하는 클래스는 CSS에 직접 정의합니다:
css
/* globals.css */
.py-1\.5 {
padding-top: 0.375rem;
padding-bottom: 0.375rem;
}