Tailwind CSS로 컴포넌트·토큰 정리하는 실전 패턴 | 디자인 시스템

Tailwind CSS로 컴포넌트·토큰 정리하는 실전 패턴 | 디자인 시스템

이 글의 핵심

tailwind.config에서 디자인 토큰과 @layer components를 정리하고, 클래스 중복을 줄이는 Tailwind CSS 프로젝트 구조 방법을 정리합니다.

들어가며

Tailwind CSS 프로젝트 구조 방법을 고민하지 않고 @apply와 임의 색상 코드를 늘리면, 몇 달 뒤에는 “이 버튼은 왜 세 가지 크기가 있지?” 상태가 됩니다. 디자인 시스템이 없는 팀이라도, 토큰(색·간격·타이포) + 컴포넌트 클래스 계층만 잡아도 유지보수 비용이 크게 줄어듭니다.

이 글은 **Tailwind v4(2026년 기준 @import "tailwindcss" 설정)**와 v3 스타일 tailwind.config 커스터마이징을 모두 염두에 두고, 실무에서 통하는 폴더·레이어·네이밍 패턴을 정리합니다.

초기 스프린트에서는 유틸리티 나열이 가장 빠르지만, 토큰과 컴포넌트 레이어를 도입하는 시점을 놓치면 리팩터링 비용이 기하급수로 커집니다. 아래 흐름으로 “언제 승격할지” 기준을 같이 잡습니다.

왜 Tailwind에 토큰·레이어가 필요한가요?

Tailwind는 유틸리티로 빠르게 UI를 완성하는 도구입니다. 다만 프로젝트가 커질수록 색 코드·간격 숫자가 JSX에 흩어지면 디자인 변경이 전역 검색·치환에 의존하게 됩니다. **theme.extend@layer**로 의미 있는 이름을 한곳에 모으면, 브랜드 갱신·다크 모드 비용이 줄어듭니다.

프로덕션에서 주의할 점

  • 동적 클래스 문자열('text-' + color)은 JIT가 못 잡는 경우가 있어, 빌드 누락으로 스타일이 사라질 수 있습니다. safelist 또는 완전한 클래스 맵을 씁니다.
  • @apply 과다는 추상화만 늘리고 재사용성은 안 늘 수 있습니다. 팀 기준(예: 같은 유틸 3회 이상 반복)을 두는 편이 안전합니다.
  • 모노레포에서는 content패키지 소스 경로를 빠뜨리면 프로덕션만 스타일 누락이 납니다.

비유로 이해하기

디자인 토큰브랜드 색·간격의 사전에 가깝고, @layer components.btn 같은 프리미티브는 자주 쓰는 문장 템플릿에 가깝습니다. 페이지 JSX는 그 템플릿을 짧게 호출만 하게 두면 읽기와 변경이 쉬워집니다.

이 글을 읽으면

  • theme.extend로 브랜드 토큰을 한곳에 모으는 법을 익힙니다
  • @layer components와 작은 UI 프리미티브로 중복을 줄입니다
  • 팀 규칙(순서, prefix, 디자인 토큰 명명)을 문서화하는 힌트를 얻습니다

목차

  1. 개념: 토큰·프리미티브·컴포넌트
  2. 실전 구현: tailwind.config와 글로벌 레이어
  3. 고급: 플러그인, 다크 모드, 컴포넌트 라이브러리
  4. 성능·비교: @apply 남용 vs 클래스 조합
  5. 실무 사례
  6. 트러블슈팅
  7. 마무리

1. 개념: 토큰·프리미티브·컴포넌트

  • 디자인 토큰: 색 팔레트, 간격 스케일, 라운드, 폰트 크기·행간 등 의미 있는 이름 → 값 매핑.
  • 프리미티브: btn, input, card처럼 여러 화면에서 재사용되는 최소 단위 스타일.
  • 페이지/기능 컴포넌트: 프리미티브를 조합한 React/Vue/Svelte 컴포넌트 — 비즈니스 로직과 스타일 경계가 여기서 맞물립니다.

Tailwind의 강점은 유틸리티로 빠르게 실험하다가, 안정된 패턴만 **토큰과 @layer**로 승격시키는 흐름입니다.


실전 구현

2-1. tailwind.config.jstheme.extend로 토큰 중앙화

// tailwind.config.js (v3 스타일 예시)
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./src/**/*.{js,ts,jsx,tsx,astro,vue,svelte}'],
  theme: {
    extend: {
      colors: {
        brand: {
          50: '#f0f9ff',
          500: '#3b82f6',
          900: '#1e3a8a',
        },
        surface: {
          DEFAULT: '#ffffff',
          muted: '#f4f4f5',
        },
      },
      spacing: {
        18: '4.5rem',
        22: '5.5rem',
      },
      borderRadius: {
        card: '0.75rem',
      },
      fontSize: {
        display: ['2.25rem', { lineHeight: '2.5rem', fontWeight: '700' }],
      },
    },
  },
  plugins: [],
};

2-2. 글로벌 CSS — 레이어 순서 고정

/* src/styles/globals.css — v4 예시 */
@import "tailwindcss";

@layer components {
  .btn {
    @apply inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium
      transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2;
  }
  .btn-primary {
    @apply btn bg-brand-500 text-white hover:bg-brand-900 focus-visible:outline-brand-500;
  }
  .btn-ghost {
    @apply btn bg-transparent text-brand-500 hover:bg-brand-50;
  }
}

2-3. 컴포넌트 파일에서의 사용

// Button.tsx
export function Button({
  variant = 'primary',
  ...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & { variant?: 'primary' | 'ghost' }) {
  const cls =
    variant === 'primary' ? 'btn-primary' : 'btn-ghost';
  return <button type="button" className={cls} {...props} />;
}

규칙 예시: components/ui/에는 프리미티브만, features/에는 도메인 컴포넌트 — Tailwind 클래스는 가능하면 프리미티브에만 @apply, 페이지에서는 클래스 문자열을 짧게 유지합니다.


고급 활용

다크 모드: class 전략 + 토큰

// tailwind.config.js
module.exports = {
  darkMode: 'class',
  // ...
};
<div className="bg-surface text-zinc-900 dark:bg-zinc-900 dark:text-zinc-50">
  ...
</div>

: surface 같은 시맨틱 토큰을 늘리면 dark: 접두사 반복이 줄어듭니다.

@tailwindcss/forms 등 플러그인

폼 요소 리셋은 팀 전체에 영향을 주므로, 플러그인 도입은 한 번에 합의하고 preflight와의 관계를 문서화하세요.


4. 성능·비교: @apply 남용 vs 클래스 조합

접근장점주의
유틸리티 직접 나열변경 추적 쉬움, JIT가 최적화마크업이 길어질 수 있음
@apply로 컴포넌트 클래스HTML 간결, 디자인 시스템에 가깝게과도하면 추상화만 늘고 재사용성은 안 늘 수 있음
CSS 변수 + Tailwind런타임 테마 전환에 유리네이밍·폴백 정책 필요

실무 규칙 예: 같은 5줄 이상의 유틸리티가 3회 이상 반복될 때만 @apply 후보로 올립니다.


실무 사례

  • 멀티 브랜드: data-theme="acme" + CSS 변수로 색 토큰만 교체, Tailwind는 bg-[var(--color-brand)] 패턴으로 연결.
  • 모노레포: packages/uitailwind.preset.js를 두고 앱에서는 presets: [require('@repo/ui/tailwind.preset')]로 공유.
  • Astro/Next: content 경로에 모든 패키지 소스를 포함해 미적용 클래스 누락을 방지합니다.

6. 트러블슈팅

흔한 실수와 해결

실수결과해결
문자열 연결로 클래스 생성프로덕션에서 스타일 누락완전한 클래스명 맵·safelist
darkMode: 'class'인데 루트에 클래스 미설정다크 스타일이 적용 안 됨html 또는 상위에 dark 클래스 토글
@apply로 복잡한 arbitrary만 이전빌드 순서·플러그인과 충돌유틸리티 직접 사용 또는 단순화
Figma와 숫자가 어긋남디자인·코드 이중 유지보수토큰 단일 출처 파이프라인 검토

JIT가 클래스를 못 찾는다

  • 동적 문자열 조합('text-' + color)은 Tailwind가 정적으로 파싱하지 못합니다. safelist 또는 완전한 클래스 이름 맵을 사용하세요.

@apply에서 arbitrary value가 깨진다

  • 플러그인 순서·@layer 밖 정의 여부를 확인하세요. 복잡하면 컴포넌트에 직접 유틸리티가 더 안전할 때가 있습니다.

디자이너 Figma 토큰과 숫자가 안 맞는다

  • Figma → JSON → Style Dictionary → Tailwind theme 파이프라인을 한 번이라도 잡으면 드리프트가 줄어듭니다.

마무리

Tailwind CSS 프로젝트 구조 방법의 핵심은 화려한 폴더 트리가 아니라, 토큰이 단일 출처(Single Source of Truth)를 갖고 레이어 규칙이 팀 합의로 남아 있는지입니다. 애니메이션·반응형과 맞물리는 내용은 HTML/CSS 시리즈 글과 함께 보면 레이아웃까지 일관되게 가져갈 수 있습니다.