Skip to content

Vue 컴포넌트 작성 패턴 가이드

이 문서는 프로젝트에서 사용하는 컴포넌트 작성 패턴을 설명합니다.

핵심 라이브러리

라이브러리용도문서
Reka UI헤드리스 UI 컴포넌트Primitive
CVA스타일 Variants 관리Getting Started
shadcn-vueUI 컴포넌트 참조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() 함수

clsxtailwind-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;
}

참고 자료