# TailwindCSS v4 스타일링 전략 Rules > Nuxt 4 + Vue 3 + TailwindCSS v4 + shadcn-vue 환경 기준 > 최종 업데이트: 2026-04-07 --- ## 빠른 참조 체크리스트 스타일링 작업 전 아래 항목을 확인한다. - [ ] 클래스 순서가 레이아웃 -> 박스 -> 타이포 -> 시각 -> 인터랙티브 순인가? - [ ] 기본 스타일이 모바일 기준이고, `sm:` / `md:` / `lg:` 순서로 확장하는가? - [ ] 동적 클래스를 문자열 보간(`` `bg-${color}-500` ``)으로 생성하지 않았는가? - [ ] 색상/간격에 하드코딩 값(`bg-[#1a1a2e]`, `mt-[17px]`) 대신 CSS 변수 또는 디자인 토큰을 사용했는가? - [ ] 다크모드 대응 시 CSS 변수(`bg-background`, `text-foreground`)를 우선 사용했는가? - [ ] `@apply`가 3개 이상 컴포넌트에서 반복되는 복합 패턴에만 사용되었는가? - [ ] 변형(variant) 처리는 `cva` + `cn()` 패턴을 사용하는가? - [ ] shadcn-vue 컴포넌트를 직접 수정하지 않고 래퍼 컴포넌트로 확장했는가? - [ ] `prettier-plugin-tailwindcss`가 설정되어 자동 정렬이 적용되는가? - [ ] `!important`(`!` prefix)를 사용하지 않았는가? --- ## 규칙 상세 ### Rule 1: 클래스 순서 (레이아웃 -> 박스 -> 타이포 -> 시각 -> 인터랙티브) 클래스는 아래 순서대로 작성한다. `prettier-plugin-tailwindcss`로 자동 정렬을 강제한다. ``` 1. 레이아웃 -> display, position, z-index, overflow 2. 박스 모델 -> width, height, margin, padding, border, rounded 3. 플렉스/그리드 -> flex-*, grid-*, gap, justify-*, items-* 4. 타이포그래피 -> font-*, text-*, leading-*, tracking-* 5. 시각 효과 -> bg-*, shadow-*, opacity-, ring-* 6. 트랜지션/애니메이션 -> transition-*, duration-*, animate-* 7. 인터랙티브 -> cursor-*, select-*, pointer-events-* 8. 상태 변형자 -> hover:, focus:, active:, disabled: 9. 반응형 변형자 -> sm:, md:, lg:, xl:, 2xl: 10. 다크모드 -> dark: ``` **DO** ```vue ``` **DON'T** ```vue ``` **Prettier 설정** ```json // .prettierrc { "plugins": ["prettier-plugin-tailwindcss"], "tailwindConfig": "./tailwind.config.ts", "tailwindFunctions": ["cn", "cva", "clsx", "twMerge"] } ``` > `tailwindFunctions`에 `cn`, `cva` 등을 등록해야 유틸 함수 내부의 클래스도 자동 정렬된다. --- ### Rule 2: Mobile-first 반응형 (기본=모바일, sm/md/lg/xl 순서) 기본 스타일은 모바일 기준으로 작성하고, 더 큰 화면에서 순서대로 덮어쓴다. ``` 기본(base) -> 0px~ : 모바일 (prefix 없음) sm -> 640px~ : 대형 모바일, 소형 태블릿 md -> 768px~ : 태블릿 lg -> 1024px~ : 소형 데스크탑 xl -> 1280px~ : 데스크탑 2xl -> 1536px~ : 대형 모니터 ``` **DO** ```vue ``` **DON'T** ```vue ``` **반응형 타이포그래피 패턴** ```vue ``` --- ### Rule 3: 컴포넌트 스타일링 (cva+cn() 패턴, @apply 기준, shadcn-vue 확장) #### cn() 유틸리티 `clsx` + `tailwind-merge`를 조합한 `cn()` 함수를 사용한다. 조건부 클래스 병합 및 Tailwind 클래스 충돌 해결을 자동으로 처리한다. ```typescript // app/lib/utils.ts import { type ClassValue, clsx } from 'clsx' import { twMerge } from 'tailwind-merge' export function cn(...inputs: ClassValue[]): string { return twMerge(clsx(inputs)) } ``` **DO** ```vue ``` **DON'T** ```vue ``` #### cva (class-variance-authority) 패턴 변형(variant)이 있는 컴포넌트는 반드시 `cva`로 정의한다. **DO** ```typescript // app/components/ui/badge/index.ts import { cva, type VariantProps } from 'class-variance-authority' export const badgeVariants = cva( // 기본 클래스 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors', { variants: { variant: { default: 'bg-primary text-primary-foreground', secondary: 'bg-secondary text-secondary-foreground', outline: 'border border-input text-foreground', destructive: 'bg-destructive text-destructive-foreground', }, }, defaultVariants: { variant: 'default' }, } ) export type BadgeVariants = VariantProps ``` ```vue ``` **DON'T** ```vue ``` #### @apply 사용 기준 `@apply`는 **3개 이상의 컴포넌트에서 반복되는 복합 패턴**에만 허용한다. **DO** ```css /* 허용: 여러 컴포넌트에서 반복 사용되는 복합 패턴 */ .btn-base { @apply inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50; } ``` **DON'T** ```css /* 금지: 단일 클래스 추상화 -> 의미 없는 간접참조 */ .text-blue { @apply text-blue-500; } .mt-small { @apply mt-2; } .flex-center { @apply flex items-center justify-center; } ``` #### shadcn-vue 커스터마이징 shadcn-vue 컴포넌트 원본 파일을 직접 수정하지 않는다. 래퍼 컴포넌트로 확장한다. **DO** ```vue ``` **DON'T** ```vue ``` --- ### Rule 4: 다크모드 (CSS 변수 우선, dark: prefix 보완용) CSS 변수 기반 테마를 우선 사용한다. `dark:` prefix는 CSS 변수로 처리할 수 없는 경우에만 보완 용도로 사용한다. **CSS 변수 정의** ```css /* app/assets/css/main.css */ @import "tailwindcss"; :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --primary: 221.2 83.2% 53.3%; --primary-foreground: 210 40% 98%; --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; --border: 214.3 31.8% 91.4%; --radius: 0.5rem; } .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; --primary: 217.2 91.2% 59.8%; --border: 217.2 32.6% 17.5%; } ``` **DO** ```vue ``` **DON'T** ```vue ``` **다크모드 토글 구현** ```vue ``` --- ### Rule 5: 상태 기반 스타일링 (hover/focus/active/disabled, group, peer) #### 기본 상태 변형자 순서 상태 변형자는 `hover` -> `focus-visible` -> `active` -> `disabled` 순서로 작성한다. **DO** ```vue ``` **DON'T** ```vue ``` #### group 패턴 (부모 상태 -> 자식 스타일) 부모 요소에 `group` 클래스를 부여하고, 자식에서 `group-hover:`, `group-focus:` 등으로 반응한다. **DO** ```vue ``` **중첩 group이 필요한 경우** `group/{name}` 문법을 사용한다. ```vue ``` #### peer 패턴 (형제 상태 -> 형제 스타일) `peer` 클래스를 가진 요소의 상태에 따라 **뒤에 오는 형제** 요소의 스타일을 변경한다. **DO** ```vue ``` **DON'T** ```vue ``` --- ### Rule 6: TailwindCSS v4 신기능 (@theme, @container 쿼리) #### @theme 디렉티브 (CSS-first 설정) TailwindCSS v4에서는 `tailwind.config.ts` 대신 CSS 파일 내 `@theme` 디렉티브로 토큰을 정의한다. **DO** ```css /* app/assets/css/main.css */ @import "tailwindcss"; @theme { /* 폰트 */ --font-sans: "Pretendard", "Noto Sans KR", system-ui, sans-serif; /* 커스텀 색상 */ --color-brand-500: oklch(60% 0.20 250); --color-brand-600: oklch(51% 0.19 250); /* 커스텀 간격 */ --spacing-18: 4.5rem; /* 커스텀 애니메이션 */ --animate-fade-in: fade-in 0.3s ease-out; --animate-slide-up: slide-up 0.4s cubic-bezier(0.16, 1, 0.3, 1); /* 커스텀 그림자 */ --shadow-soft: 0 2px 8px rgb(0 0 0 / 0.08), 0 1px 3px rgb(0 0 0 / 0.06); } @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes slide-up { from { transform: translateY(8px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } ``` ```vue ``` **DON'T** ```css /* 금지: v4에서 @theme 대신 옛 방식(tailwind.config.ts extend)으로 토큰 정의 */ /* tailwind.config.ts의 theme.extend는 여전히 동작하지만 @theme이 권장 방식 */ ``` #### @container 쿼리 뷰포트가 아닌 **부모 컨테이너 크기** 기반으로 반응형 스타일을 적용한다. 사이드바, 카드 리스트 등 배치 위치에 따라 크기가 달라지는 컴포넌트에 유용하다. **DO** ```vue ``` **DON'T** ```vue ``` --- ### Rule 7: 성능 최적화 (동적 클래스 생성 금지, 클래스 매핑 패턴) TailwindCSS v4는 소스 코드를 정적 분석하여 사용된 클래스만 번들에 포함한다. 런타임에 문자열을 조합하면 빌드 시 해당 클래스가 누락된다. #### 동적 클래스 생성 금지 (Critical) **DO** ```vue ``` **DON'T** ```vue ``` #### 조건부 클래스 패턴 **DO** ```vue ``` **DON'T** ```vue ``` --- ### Rule 8: 재사용 패턴 (디자인 토큰, 상태 클래스 매핑) #### 디자인 토큰 (CSS 변수 체계) 프로젝트 전역에서 사용하는 레이아웃 수치, z-index, 트랜지션 등은 CSS 변수로 중앙 관리한다. ```css /* app/assets/css/tokens.css */ :root { /* 레이아웃 */ --header-height: 4rem; --sidebar-width: 16rem; --content-max-width: 75rem; /* Z-index 계층 */ --z-dropdown: 100; --z-sticky: 200; --z-modal: 400; --z-toast: 600; --z-tooltip: 700; /* 트랜지션 */ --duration-fast: 100ms; --duration-base: 200ms; --ease-spring: cubic-bezier(0.16, 1, 0.3, 1); } ``` ```vue ``` #### 상태별 클래스 매핑 반복되는 상태 스타일을 타입 안전한 매핑 객체로 관리한다. **DO** ```typescript // app/utils/statusStyles.ts type StatusVariant = 'success' | 'warning' | 'error' | 'info' | 'neutral' export const STATUS_STYLES: Record = { success: { badge: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400', text: 'text-emerald-700 dark:text-emerald-400', }, warning: { badge: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400', text: 'text-amber-700 dark:text-amber-400', }, error: { badge: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400', text: 'text-red-700 dark:text-red-400', }, info: { badge: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400', text: 'text-blue-700 dark:text-blue-400', }, neutral: { badge: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-400', text: 'text-gray-700 dark:text-gray-400', }, } as const ``` ```vue ``` **DON'T** ```vue ``` --- ### Rule 9: 금지사항 (Anti-patterns 코드 예시) #### 1. 인라인 스타일 사용 금지 ```vue
콘텐츠
콘텐츠
``` > 예외: JS로 계산된 동적 수치(드래그 위치, 애니메이션 좌표 등)는 인라인 스타일 허용. #### 2. 동적 클래스 문자열 생성 금지 ```typescript // DON'T const cls = `bg-${color}-500` const size = `text-${props.size}` // DO const COLOR_MAP = { blue: 'bg-blue-500', red: 'bg-red-500' } as const const cls = COLOR_MAP[color] ``` #### 3. !important 남용 금지 ```vue
강제 오버라이드
올바른 병합
``` > `!important`가 필요한 상황은 대부분 클래스 적용 순서 문제. `cn()`이나 구조 변경으로 해결한다. #### 4. @apply 과용 금지 ```css /* DON'T: 단일 유틸리티 추상화 */ .text-blue { @apply text-blue-500; } .mt-small { @apply mt-2; } .flex-center { @apply flex items-center justify-center; } /* DO: 3개 이상 컴포넌트에서 공유하는 복합 패턴에만 사용 */ .input-base { @apply w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring; } ``` #### 5. Magic number 임의값 남용 금지 ```vue
디자인 시스템 무시
디자인 시스템 준수
``` > 임의값(`[...]`)은 디자인 토큰에 없는 값이 **정말로** 필요한 경우에만 최소한으로 사용한다. 반복되면 `@theme`에 토큰으로 등록한다. #### 6. 하드코딩 색상 금지 ```vue
하드코딩 색상
CSS 변수 사용
``` #### 7. 불필요한 반응형 클래스 반복 금지 ```vue

텍스트

텍스트

``` --- ## 자주 하는 실수 TOP 5 ### 1. 문자열 보간으로 동적 클래스 생성 가장 흔하고 치명적인 실수. TailwindCSS v4의 정적 분석에 감지되지 않아 프로덕션에서 스타일이 누락된다. ```typescript // 실수 const colorClass = `text-${props.color}-500` // 빌드 시 누락 // 해결 const COLOR_MAP: Record = { blue: 'text-blue-500', red: 'text-red-500', } as const const colorClass = computed(() => COLOR_MAP[props.color] ?? 'text-blue-500') ``` ### 2. Desktop-first로 반응형 작성 큰 화면 기준으로 먼저 작성하고 `max-*`로 축소하면 모바일에서 예상치 못한 스타일 충돌이 발생한다. ```vue
``` ### 3. CSS 변수 대신 dark: prefix 남발 모든 요소에 `dark:` 변형을 달면 코드량이 2배로 늘어나고 관리가 어려워진다. ```vue
``` ### 4. cn() 없이 외부 클래스를 문자열로 합침 `cn()` 없이 단순 문자열 연결을 하면 Tailwind 클래스 간 충돌(예: `p-4`와 `p-2`)이 해결되지 않아 예측 불가능한 스타일이 적용된다. ```vue
``` ### 5. focus 대신 focus-visible 사용 누락 `focus:`는 마우스 클릭에도 포커스 링이 표시되어 시각적 노이즈를 만든다. `focus-visible:`은 키보드 탐색 시에만 표시된다. ```vue