Tailwind CSS v4로 프로덕션 디자인 시스템을 구축합니다. 토큰 계층 설계, 컴포넌트 라이브러리, 테마 시스템, 접근성, 팀 협업 전략을 실전 중심으로 다룹니다.
9장에서 커스텀 변형과 플러그인 시스템을 배웠습니다. 이 장에서는 시리즈 전체에서 다룬 기능을 종합하여 프로덕션 수준의 디자인 시스템을 구축합니다. 토큰 설계, 컴포넌트 라이브러리, 테마 시스템, 접근성을 포괄하는 체계적인 접근법을 다룹니다.
잘 설계된 디자인 시스템은 토큰을 세 계층으로 구분합니다.
원시 토큰 (Primitive) → 의미론적 토큰 (Semantic) → 컴포넌트 토큰 (Component)
oklch(0.62 0.21 260) → --color-primary → --color-btn-primary
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
/* === 1계층: 원시 토큰 === */
@theme {
/* 색상 팔레트 */
--color-blue-50: oklch(0.97 0.01 250);
--color-blue-100: oklch(0.94 0.03 250);
--color-blue-200: oklch(0.88 0.06 250);
--color-blue-300: oklch(0.80 0.11 250);
--color-blue-400: oklch(0.70 0.17 250);
--color-blue-500: oklch(0.60 0.22 250);
--color-blue-600: oklch(0.53 0.24 252);
--color-blue-700: oklch(0.46 0.22 254);
--color-blue-800: oklch(0.39 0.18 256);
--color-blue-900: oklch(0.33 0.13 258);
--color-blue-950: oklch(0.25 0.08 260);
--color-neutral-50: oklch(0.985 0.002 250);
--color-neutral-100: oklch(0.97 0.004 250);
--color-neutral-200: oklch(0.92 0.006 250);
--color-neutral-300: oklch(0.87 0.008 250);
--color-neutral-400: oklch(0.71 0.01 250);
--color-neutral-500: oklch(0.55 0.012 250);
--color-neutral-600: oklch(0.45 0.012 250);
--color-neutral-700: oklch(0.37 0.01 250);
--color-neutral-800: oklch(0.27 0.008 250);
--color-neutral-900: oklch(0.21 0.006 250);
--color-neutral-950: oklch(0.13 0.004 250);
/* 간격 스케일 */
--spacing-0: 0;
--spacing-0.5: 0.125rem;
--spacing-1: 0.25rem;
--spacing-1.5: 0.375rem;
--spacing-2: 0.5rem;
--spacing-3: 0.75rem;
--spacing-4: 1rem;
--spacing-5: 1.25rem;
--spacing-6: 1.5rem;
--spacing-8: 2rem;
--spacing-10: 2.5rem;
--spacing-12: 3rem;
--spacing-16: 4rem;
--spacing-20: 5rem;
--spacing-24: 6rem;
/* 글꼴 */
--font-sans: "Pretendard Variable", system-ui, sans-serif;
--font-mono: "JetBrains Mono", "Fira Code", monospace;
/* 둥근 모서리 */
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--radius-2xl: 1rem;
--radius-full: 9999px;
}@theme {
/* 표면/배경 */
--color-background: var(--color-neutral-50);
--color-surface: white;
--color-surface-alt: var(--color-neutral-100);
/* 텍스트 */
--color-foreground: var(--color-neutral-900);
--color-foreground-muted: var(--color-neutral-500);
--color-foreground-subtle: var(--color-neutral-400);
/* 테두리 */
--color-border: var(--color-neutral-200);
--color-border-strong: var(--color-neutral-300);
/* 인터랙티브 */
--color-primary: var(--color-blue-500);
--color-primary-hover: var(--color-blue-600);
--color-primary-active: var(--color-blue-700);
--color-primary-foreground: white;
--color-primary-subtle: var(--color-blue-50);
/* 상태 */
--color-success: oklch(0.65 0.19 150);
--color-success-subtle: oklch(0.95 0.05 150);
--color-warning: oklch(0.80 0.15 80);
--color-warning-subtle: oklch(0.95 0.04 80);
--color-error: oklch(0.58 0.24 25);
--color-error-subtle: oklch(0.95 0.05 25);
/* 그림자 */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.07), 0 2px 4px -2px rgb(0 0 0 / 0.07);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.08), 0 4px 6px -4px rgb(0 0 0 / 0.08);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.08), 0 8px 10px -6px rgb(0 0 0 / 0.08);
}.dark {
--color-background: var(--color-neutral-950);
--color-surface: var(--color-neutral-900);
--color-surface-alt: var(--color-neutral-800);
--color-foreground: var(--color-neutral-100);
--color-foreground-muted: var(--color-neutral-400);
--color-foreground-subtle: var(--color-neutral-500);
--color-border: var(--color-neutral-700);
--color-border-strong: var(--color-neutral-600);
--color-primary: var(--color-blue-400);
--color-primary-hover: var(--color-blue-300);
--color-primary-active: var(--color-blue-200);
--color-primary-foreground: var(--color-neutral-950);
--color-primary-subtle: oklch(0.22 0.05 260);
--color-success-subtle: oklch(0.22 0.04 150);
--color-warning-subtle: oklch(0.25 0.04 80);
--color-error-subtle: oklch(0.22 0.04 25);
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.4);
}@layer components {
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--spacing-2);
font-weight: 500;
border-radius: var(--radius-md);
transition: all 0.15s ease;
cursor: pointer;
outline: none;
&:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
/* 크기 변형 */
.btn-sm { padding: var(--spacing-1) var(--spacing-3); font-size: 0.875rem; }
.btn-md { padding: var(--spacing-2) var(--spacing-4); font-size: 0.875rem; }
.btn-lg { padding: var(--spacing-3) var(--spacing-6); font-size: 1rem; }
/* 스타일 변형 */
.btn-primary {
background-color: var(--color-primary);
color: var(--color-primary-foreground);
&:hover:not(:disabled) { background-color: var(--color-primary-hover); }
&:active:not(:disabled) { background-color: var(--color-primary-active); }
}
.btn-secondary {
background-color: var(--color-surface-alt);
color: var(--color-foreground);
&:hover:not(:disabled) { background-color: var(--color-border); }
}
.btn-outline {
border: 1px solid var(--color-border);
background-color: transparent;
color: var(--color-foreground);
&:hover:not(:disabled) { background-color: var(--color-surface-alt); }
}
.btn-ghost {
background-color: transparent;
color: var(--color-foreground);
&:hover:not(:disabled) { background-color: var(--color-surface-alt); }
}
.btn-danger {
background-color: var(--color-error);
color: white;
&:hover:not(:disabled) { background-color: oklch(0.52 0.25 25); }
}
}@layer components {
.card {
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.card-header {
padding: var(--spacing-4) var(--spacing-6);
border-bottom: 1px solid var(--color-border);
}
.card-body {
padding: var(--spacing-6);
}
.card-footer {
padding: var(--spacing-4) var(--spacing-6);
border-top: 1px solid var(--color-border);
}
}@layer components {
.input {
display: block;
width: 100%;
padding: var(--spacing-2) var(--spacing-3);
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
font-size: 0.875rem;
color: var(--color-foreground);
transition: border-color 0.15s ease, box-shadow 0.15s ease;
&::placeholder {
color: var(--color-foreground-subtle);
}
&:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px var(--color-primary-subtle);
}
&:disabled {
background-color: var(--color-surface-alt);
opacity: 0.7;
cursor: not-allowed;
}
&[aria-invalid="true"] {
border-color: var(--color-error);
&:focus {
box-shadow: 0 0 0 3px var(--color-error-subtle);
}
}
}
.label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: var(--color-foreground);
margin-bottom: var(--spacing-1);
}
.input-error {
font-size: 0.75rem;
color: var(--color-error);
margin-top: var(--spacing-1);
}
}@utility page-container {
width: 100%;
max-width: 1280px;
margin-inline: auto;
padding-inline: var(--spacing-4);
@media (min-width: 768px) {
padding-inline: var(--spacing-6);
}
@media (min-width: 1024px) {
padding-inline: var(--spacing-8);
}
}
@utility prose-container {
width: 100%;
max-width: 768px;
margin-inline: auto;
}@utility stack-sm {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
}
@utility stack-md {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
}
@utility stack-lg {
display: flex;
flex-direction: column;
gap: var(--spacing-6);
}@layer base {
:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* Skip Navigation */
.skip-nav {
position: absolute;
top: var(--spacing-4);
left: var(--spacing-4);
padding: var(--spacing-2) var(--spacing-4);
background-color: var(--color-primary);
color: var(--color-primary-foreground);
border-radius: var(--radius-md);
font-weight: 500;
z-index: 100;
transform: translateY(-200%);
transition: transform 0.2s ease;
&:focus {
transform: translateY(0);
}
}
}@layer base {
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
}styles/
globals.css # 진입점
theme/
primitives.css # 원시 토큰
semantic.css # 의미론적 토큰
dark.css # 다크 모드 오버라이드
base/
reset.css # 기본 리셋
typography.css # 타이포그래피
accessibility.css # 접근성
components/
buttons.css # 버튼
cards.css # 카드
forms.css # 폼 요소
navigation.css # 네비게이션
utilities/
layout.css # 레이아웃 유틸리티
custom.css # 커스텀 유틸리티
@import "tailwindcss";
/* 테마 */
@import "./theme/primitives.css";
@import "./theme/semantic.css";
@import "./theme/dark.css";
/* 기본 */
@import "./base/reset.css" layer(base);
@import "./base/typography.css" layer(base);
@import "./base/accessibility.css" layer(base);
/* 컴포넌트 */
@import "./components/buttons.css" layer(components);
@import "./components/cards.css" layer(components);
@import "./components/forms.css" layer(components);
@import "./components/navigation.css" layer(components);
/* 유틸리티 */
@import "./utilities/layout.css";
@import "./utilities/custom.css";
/* 플러그인 */
@plugin "@tailwindcss/typography";
/* 변형 */
@custom-variant dark (&:where(.dark, .dark *));디자인 시스템의 파일 구조는 토큰 → 기본 → 컴포넌트 → 유틸리티 순서를 따릅니다. 이 순서는 CSS의 캐스케이드와 일치하여, 나중에 정의된 스타일이 자연스럽게 높은 우선순위를 가집니다.
Tailwind CSS v4로 디자인 시스템을 구축할 때 핵심은 3계층 토큰 아키텍처(원시 → 의미론적 → 컴포넌트)를 기반으로, CSS 레이어로 우선순위를 관리하고, 커스텀 프로퍼티로 런타임 테마 전환을 지원하는 것입니다. 접근성과 모션 감소 대응을 기본으로 포함하고, 체계적인 파일 구조로 팀 협업을 지원합니다.
다음 마지막 장에서는 기존 Tailwind v3 프로젝트를 v4로 마이그레이션하는 실전 가이드를 다룹니다. 주요 변경사항, 자동 마이그레이션 도구, 그리고 단계별 전환 전략을 배웁니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
Tailwind CSS v3에서 v4로의 마이그레이션 가이드 — 주요 변경사항, 자동 마이그레이션 도구, 단계별 전환 전략, 그리고 흔한 문제 해결법을 다룹니다.
Tailwind CSS v4의 @custom-variant, @plugin 디렉티브, CSS 기반 확장 시스템, 그리고 기존 JS 플러그인에서의 마이그레이션을 실전 중심으로 다룹니다.
Tailwind CSS v4의 다크 모드 전략, CSS 커스텀 프로퍼티 기반 런타임 테마 전환, 다중 테마 패턴, 그리고 next-themes와의 통합을 실전 중심으로 다룹니다.