Tailwind CSS로 디자인 시스템 구축하기

Tailwind CSS를 활용해서 확장 가능하고 일관성 있는 디자인 시스템을 구축한 경험을 공유합니다.

Tailwind CSS디자인 시스템CSSFrontend
--
Tailwind CSS로 디자인 시스템 구축하기

최근 회사에서 여러 프로젝트에 걸쳐 사용할 수 있는 디자인 시스템을 구축하게 되었습니다. Tailwind CSS를 기반으로 한 디자인 시스템 구축 과정과 노하우를 공유하겠습니다.

🎨 디자인 시스템이 필요한 이유

기존 문제점들

  • 일관성 부족: 프로젝트마다 다른 색상, 간격, 타이포그래피
  • 유지보수 어려움: 변경사항이 생기면 모든 곳을 수정해야 함
  • 개발 속도 저하: 매번 스타일을 새로 작성해야 함
  • 협업 비효율: 디자이너와 개발자 간 소통 비용 증가

디자인 시스템의 장점

  • 일관된 사용자 경험 제공
  • 개발 속도 향상 (재사용 가능한 컴포넌트)
  • 유지보수성 개선
  • 확장성 확보

🏗️ 구축 과정

1. Tailwind Config 기본 설정

먼저 프로젝트의 브랜드 컬러와 기본 설정을 정의했습니다.

// tailwind.config.js
module.exports = {
  content: ["./src/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {
      colors: {
        primary: {
          50: "#eff6ff",
          100: "#dbeafe",
          200: "#bfdbfe",
          300: "#93c5fd",
          400: "#60a5fa",
          500: "#3b82f6", // Main brand color
          600: "#2563eb",
          700: "#1d4ed8",
          800: "#1e40af",
          900: "#1e3a8a",
        },
        gray: {
          50: "#f9fafb",
          100: "#f3f4f6",
          200: "#e5e7eb",
          300: "#d1d5db",
          400: "#9ca3af",
          500: "#6b7280",
          600: "#4b5563",
          700: "#374151",
          800: "#1f2937",
          900: "#111827",
        },
        success: "#10b981",
        warning: "#f59e0b",
        error: "#ef4444",
      },
      fontFamily: {
        sans: ["Pretendard", "system-ui", "sans-serif"],
        mono: ["JetBrains Mono", "monospace"],
      },
      spacing: {
        18: "4.5rem",
        88: "22rem",
      },
      borderRadius: {
        xl: "1rem",
        "2xl": "1.5rem",
      },
    },
  },
  plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography")],
};

2. 컴포넌트 라이브러리 구축

재사용 가능한 컴포넌트들을 만들었습니다.

Button 컴포넌트

// components/Button.tsx
import { ButtonHTMLAttributes, ReactNode } from "react";
import { cva, type VariantProps } from "class-variance-authority";

const buttonVariants = cva(
  // 기본 스타일
  "inline-flex items-center justify-center rounded-lg font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none",
  {
    variants: {
      variant: {
        primary: "bg-primary-500 text-white hover:bg-primary-600 focus:ring-primary-500",
        secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500",
        outline: "border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 focus:ring-primary-500",
        ghost: "text-gray-700 hover:bg-gray-100 focus:ring-gray-500",
        danger: "bg-red-500 text-white hover:bg-red-600 focus:ring-red-500",
      },
      size: {
        sm: "h-8 px-3 text-sm",
        md: "h-10 px-4 text-base",
        lg: "h-12 px-6 text-lg",
        xl: "h-14 px-8 text-xl",
      },
    },
    defaultVariants: {
      variant: "primary",
      size: "md",
    },
  }
);

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
  children: ReactNode;
  loading?: boolean;
}

export function Button({ children, variant, size, loading, className, ...props }: ButtonProps) {
  return (
    <button className={buttonVariants({ variant, size, className })} disabled={loading} {...props}>
      {loading && (
        <svg className="w-4 h-4 mr-2 animate-spin" viewBox="0 0 24 24">
          <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
          <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
        </svg>
      )}
      {children}
    </button>
  );
}

Input 컴포넌트

// components/Input.tsx
import { InputHTMLAttributes, forwardRef } from "react";
import { cva, type VariantProps } from "class-variance-authority";

const inputVariants = cva("block w-full rounded-lg border transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2", {
  variants: {
    variant: {
      default: "border-gray-300 focus:border-primary-500 focus:ring-primary-500",
      error: "border-red-500 focus:border-red-500 focus:ring-red-500",
      success: "border-green-500 focus:border-green-500 focus:ring-green-500",
    },
    size: {
      sm: "px-3 py-2 text-sm",
      md: "px-4 py-3 text-base",
      lg: "px-5 py-4 text-lg",
    },
  },
  defaultVariants: {
    variant: "default",
    size: "md",
  },
});

interface InputProps extends InputHTMLAttributes<HTMLInputElement>, VariantProps<typeof inputVariants> {
  label?: string;
  helperText?: string;
  error?: string;
}

export const Input = forwardRef<HTMLInputElement, InputProps>(({ label, helperText, error, variant, size, className, ...props }, ref) => {
  const inputVariant = error ? "error" : variant;

  return (
    <div className="space-y-1">
      {label && <label className="block text-sm font-medium text-gray-700">{label}</label>}
      <input ref={ref} className={inputVariants({ variant: inputVariant, size, className })} {...props} />
      {error && <p className="text-sm text-red-600">{error}</p>}
      {helperText && !error && <p className="text-sm text-gray-500">{helperText}</p>}
    </div>
  );
});

Input.displayName = "Input";

3. 레이아웃 시스템

일관된 레이아웃을 위한 컴포넌트들을 만들었습니다.

// components/Layout.tsx
interface ContainerProps {
  children: React.ReactNode;
  size?: "sm" | "md" | "lg" | "xl" | "full";
}

export function Container({ children, size = "lg" }: ContainerProps) {
  const sizeClasses = {
    sm: "max-w-screen-sm",
    md: "max-w-screen-md",
    lg: "max-w-screen-lg",
    xl: "max-w-screen-xl",
    full: "max-w-full",
  };

  return <div className={`mx-auto px-4 sm:px-6 lg:px-8 ${sizeClasses[size]}`}>{children}</div>;
}

// 그리드 시스템
export function Grid({ children, cols = 1, gap = "md" }: { children: React.ReactNode; cols?: 1 | 2 | 3 | 4 | 6 | 12; gap?: "sm" | "md" | "lg" }) {
  const colsClasses = {
    1: "grid-cols-1",
    2: "grid-cols-1 md:grid-cols-2",
    3: "grid-cols-1 md:grid-cols-2 lg:grid-cols-3",
    4: "grid-cols-1 md:grid-cols-2 lg:grid-cols-4",
    6: "grid-cols-2 md:grid-cols-3 lg:grid-cols-6",
    12: "grid-cols-4 md:grid-cols-6 lg:grid-cols-12",
  };

  const gapClasses = {
    sm: "gap-2",
    md: "gap-4",
    lg: "gap-6",
  };

  return <div className={`grid ${colsClasses[cols]} ${gapClasses[gap]}`}>{children}</div>;
}

4. 유틸리티 클래스 확장

자주 사용하는 패턴들을 유틸리티 클래스로 만들었습니다.

/* styles/utilities.css */
@layer utilities {
  /* Card 스타일 */
  .card {
    @apply bg-white rounded-xl border border-gray-200 shadow-sm;
  }

  .card-hover {
    @apply card transition-shadow hover:shadow-md;
  }

  /* Text 스타일 */
  .text-gradient {
    @apply bg-gradient-to-r from-primary-600 to-blue-600 bg-clip-text text-transparent;
  }

  /* Animation */
  .fade-in {
    @apply opacity-0 animate-pulse;
    animation: fadeIn 0.5s ease-in-out forwards;
  }

  /* Focus styles */
  .focus-ring {
    @apply focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2;
  }
}

@keyframes fadeIn {
  to {
    opacity: 1;
  }
}

📱 반응형 디자인 전략

1. 모바일 퍼스트 접근

// 모바일 우선으로 설계
<div
  className="
  p-4 text-sm              // 모바일 기본
  sm:p-6 sm:text-base      // 작은 화면 (640px+)
  md:p-8 md:text-lg        // 중간 화면 (768px+)
  lg:p-12 lg:text-xl       // 큰 화면 (1024px+)
  xl:p-16 xl:text-2xl      // 매우 큰 화면 (1280px+)
"
>
  Responsive content
</div>

2. 컨테이너 쿼리 활용

@layer utilities {
  .card-responsive {
    @apply p-4;

    @container (min-width: 300px) {
      @apply p-6;
    }

    @container (min-width: 500px) {
      @apply p-8;
    }
  }
}

🔧 개발 도구 및 워크플로우

1. Storybook 도입

컴포넌트 문서화와 테스트를 위해 Storybook을 사용했습니다.

// Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";

const meta: Meta<typeof Button> = {
  title: "Components/Button",
  component: Button,
  parameters: {
    layout: "centered",
  },
  tags: ["autodocs"],
};

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
  args: {
    variant: "primary",
    children: "Primary Button",
  },
};

export const AllVariants: Story = {
  render: () => (
    <div className="space-x-4">
      <Button variant="primary">Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="danger">Danger</Button>
    </div>
  ),
};

2. 디자인 토큰 자동화

// scripts/generate-tokens.js
const fs = require("fs");
const config = require("../tailwind.config.js");

function generateTokens() {
  const tokens = {
    colors: config.theme.extend.colors,
    spacing: config.theme.extend.spacing,
    fontFamily: config.theme.extend.fontFamily,
  };

  // CSS 변수로 출력
  const cssVars = Object.entries(tokens.colors.primary)
    .map(([key, value]) => `  --color-primary-${key}: ${value};`)
    .join("\n");

  const css = `:root {\n${cssVars}\n}`;

  fs.writeFileSync("./src/styles/tokens.css", css);
  console.log("✅ Design tokens generated!");
}

generateTokens();

📊 성과 측정

Before vs After

지표도입 전도입 후개선율
CSS 번들 크기284KB95KB66% ⬇️
컴포넌트 개발 시간2-3시간30분75% ⬇️
디자인 일관성60%95%58% ⬆️
반응형 버그주 5-6개주 1-2개70% ⬇️

개발자 경험 개선

  • 코드 리뷰 시간 단축: 스타일 관련 리뷰 시간 50% 감소
  • 신규 개발자 온보딩: 학습 곡선 완만화
  • 유지보수성: 버그 수정 시간 60% 단축

🎯 모범 사례와 팁

1. 클래스명 순서 규칙

// 권장 순서: 레이아웃 → 박스모델 → 타이포그래피 → 비주얼 → 기타
<div className="
  flex items-center justify-between    // 레이아웃
  w-full h-16 p-4 m-2                 // 박스모델
  text-lg font-medium                 // 타이포그래피
  bg-white border border-gray-200     // 비주얼
  transition-colors hover:bg-gray-50  // 기타
">

2. 조건부 스타일링

import { clsx } from "clsx";

function Badge({ variant, size }: BadgeProps) {
  return (
    <span
      className={clsx("inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium", {
        "bg-blue-100 text-blue-800": variant === "info",
        "bg-green-100 text-green-800": variant === "success",
        "bg-red-100 text-red-800": variant === "error",
        "px-2 py-1 text-xs": size === "sm",
        "px-3 py-1.5 text-sm": size === "md",
      })}
    >
      {children}
    </span>
  );
}

3. 다크모드 지원

// tailwind.config.js
module.exports = {
  darkMode: "class",
  theme: {
    extend: {
      colors: {
        background: {
          light: "#ffffff",
          dark: "#1f2937",
        },
      },
    },
  },
};
// 다크모드 스타일링
<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">Content</div>

🚀 앞으로의 계획

1. 고도화 계획

  • 애니메이션 라이브러리 통합 (Framer Motion)
  • 접근성 개선 (ARIA 속성, 키보드 내비게이션)
  • 테마 시스템 확장 (다중 브랜드 지원)

2. 도구 개선

  • Figma 토큰 연동 (Design Tokens Studio)
  • 자동 테스팅 (Visual Regression Testing)
  • 성능 모니터링 대시보드

3. 팀 확산

  • 디자인 시스템 워크샵 진행
  • 가이드라인 문서 작성
  • 컴포넌트 리뷰 프로세스 구축

💭 마무리

Tailwind CSS 기반 디자인 시스템 구축을 통해 개발 효율성과 일관성을 크게 향상시킬 수 있었습니다. 특히 utility-first 접근법은 빠른 프로토타이핑과 유지보수에 큰 도움이 되었습니다.

중요한 것은 처음부터 완벽한 시스템을 만들려 하지 말고, 팀의 니즈에 맞게 점진적으로 발전시켜 나가는 것입니다.

다음 포스트에서는 Headless UI와 Radix UI를 활용한 고급 컴포넌트 패턴에 대해 다뤄보겠습니다! 🎨✨

댓글

0/2000
Newsletter

이 글이 도움이 되셨나요?

새로운 글이 발행되면 이메일로 알려드립니다.

뉴스레터 구독하기