Tailwind CSS 컴포넌트 스타일링
18.3.1. 컴포넌트별 스타일 패턴
컴포넌트 스타일링의 기본 원칙은 유틸리티 클래스 직접 사용입니다.
vue
<template>
<div class="flex items-center gap-4 rounded-lg border border-gray-200 p-4">
<img
:src="user.avatar"
:alt="user.name"
class="h-12 w-12 rounded-full object-cover"
/>
<div>
<p class="text-sm font-semibold text-gray-900">{{ user.name }}</p>
<p class="text-xs text-gray-500">{{ user.email }}</p>
</div>
</div>
</template>- 일반 페이지 컴포넌트에서는 템플릿에 유틸리티 클래스를 직접 작성합니다.
@apply는 3회 이상 반복되는 재사용 컴포넌트에서만 허용합니다.<style scoped>블록에 커스텀 CSS를 작성하는 것은 최소화합니다.
18.3.2. 동적 클래스 바인딩
Vue의 :class 디렉티브를 사용하여 조건부 클래스를 적용합니다.
객체 문법
vue
<template>
<button
:class="{
'bg-brand-600 text-white': isActive,
'bg-gray-100 text-gray-700': !isActive,
}"
class="rounded-md px-4 py-2 text-sm font-medium"
@click="toggle"
>
{{ label }}
</button>
</template>배열 문법
vue
<template>
<div :class="[baseClass, sizeClass, isDisabled ? 'opacity-50 cursor-not-allowed' : '']">
<slot />
</div>
</template>computed 활용
클래스 조합이 복잡한 경우 computed로 분리합니다.
vue
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
status: 'success' | 'warning' | 'error'
}>()
const statusClass = computed(() => {
const classMap: Record<string, string> = {
success: 'bg-green-100 text-green-800 border-green-200',
warning: 'bg-yellow-100 text-yellow-800 border-yellow-200',
error: 'bg-red-100 text-red-800 border-red-200',
}
return classMap[props.status]
})
</script>
<template>
<span :class="statusClass" class="inline-flex rounded-full border px-3 py-1 text-xs font-medium">
<slot />
</span>
</template>- 정적 클래스는
class속성에, 동적 클래스는:class에 분리하여 작성합니다. - 3개 이상의 조건 분기가 존재하면
computed로 추출해야 합니다.
18.3.3. 변형 패턴
재사용 컴포넌트는 props를 통해 스타일 변형을 제공합니다.
vue
<script setup lang="ts">
import { computed } from 'vue'
const props = withDefaults(
defineProps<{
size?: 'sm' | 'md' | 'lg'
variant?: 'primary' | 'secondary' | 'outline'
}>(),
{
size: 'md',
variant: 'primary',
},
)
const sizeClass = computed(() => {
const map: Record<string, string> = {
sm: 'px-3 py-1.5 text-xs',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base',
}
return map[props.size]
})
const variantClass = computed(() => {
const map: Record<string, string> = {
primary: 'bg-brand-600 text-white hover:bg-brand-700',
secondary: 'bg-gray-600 text-white hover:bg-gray-700',
outline: 'border border-brand-600 text-brand-600 hover:bg-brand-50',
}
return map[props.variant]
})
</script>
<template>
<button
:class="[sizeClass, variantClass]"
class="inline-flex items-center justify-center rounded-md font-medium transition-colors
focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2"
>
<slot />
</button>
</template>- 변형 값은 유니온 타입으로 제한하여 임의 값 전달을 방지합니다.
- 클래스 맵은
Record타입을 사용하여 타입 안전성을 확보합니다. withDefaults로 기본값을 명시하여 props 누락 시에도 안정적으로 렌더링합니다.
18.3.4. 다크 모드
Tailwind CSS의 dark: 접두사를 사용하여 다크 모드를 구현합니다.
tailwind.config.ts에서 darkMode를 'class'로 설정합니다.
ts
// tailwind.config.ts
export default {
darkMode: 'class',
// ...
} satisfies Configmedia전략 대신class전략을 사용해야 합니다.class전략은 사용자 선택을 존중합니다.
다크 모드 클래스 적용
vue
<template>
<div class="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<h2 class="text-lg font-bold text-gray-800 dark:text-gray-200">
제목
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400">
본문 내용입니다.
</p>
</div>
</template>테마 토글 구현
vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const isDark = ref(false)
function toggleTheme() {
isDark.value = !isDark.value
document.documentElement.classList.toggle('dark', isDark.value)
localStorage.setItem('theme', isDark.value ? 'dark' : 'light')
}
onMounted(() => {
const saved = localStorage.getItem('theme')
isDark.value = saved === 'dark'
document.documentElement.classList.toggle('dark', isDark.value)
})
</script>
<template>
<button
class="rounded-md p-2 text-gray-600 hover:bg-gray-100
dark:text-gray-400 dark:hover:bg-gray-800"
@click="toggleTheme"
>
{{ isDark ? '라이트 모드' : '다크 모드' }}
</button>
</template>localStorage에 사용자 선택을 저장하여 새로고침 시에도 테마를 유지합니다.<html>요소에dark클래스를 토글하여 전체 페이지에 적용합니다.