본문으로 건너뛰기
Previous
Next
Tailwind CSS 완벽 가이드: 유틸리티 우선 CSS 프레임워크

Tailwind CSS 완벽 가이드: 유틸리티 우선 CSS 프레임워크

Tailwind CSS 완벽 가이드: 유틸리티 우선 CSS 프레임워크

이 글의 핵심

Tailwind CSS는 유틸리티 우선 접근 방식으로 HTML에서 직접 스타일링하는 현대적인 CSS 프레임워크입니다. JIT(Just-In-Time) 컴파일러로 필요한 CSS만 생성하여 번들 크기를 최소화하고 빌드 속도를 향상시킵니다. 설정 파일을 통한 완벽한 커스터마이징과 디자인 시스템 구축이 가능합니다.

Tailwind CSS란?

솔직히 말하면, 처음엔 지저분해 보이는데… 익숙해지면 이게 제일 빠르다는 쪽으로 기우는 프레임워크가 Tailwind예요. 유틸리티 우선(Utility-First)이라는 말은, 작은 클래스들을 HTML에서 조합해서 화면을 만든다는 뜻이에요. “CSS는 styles/에 있고 마크업은 저기…” 이런 컨텍스트 스위칭을 줄이는 쪽에 초점이 있죠.

핵심 특징

  1. 유틸리티 우선

    • 한 클래스가 한 역할만 한다는 전제
    • 스타일을 “추상 문장”이 아니라 “조합”으로 쌓는 느낌
    • 나는 여전히 큰 덩어리 CSS를 싫어하는 편이라, 이 점이 좋았어요
  2. JIT(그때그때 생성)

    • 쓴 것만 빌드 결과에 남는다는 느낌에 가깝고
    • 개발 중에도 반응이 빠른 편
    • 번들이 작게 유지되는 건 “잘 스캔되고 + 동적 클래스 안 쓰면”이 전제예요
  3. 커스터마이징

    • tailwind.config나 v4의 CSS 쪽 테마로 팀 색·간격을 못 박을 수 있음
    • “우리만의 디자인 시스템”을 코드에 박아 넣기 좋음
  4. 요즘 CSS

    • Grid, Flexbox, 변수, Container Queries 같은 걸 유틸로 끌고 올 수 있음

Bootstrap이랑 뭔가 다르지?

표로 박아 비교하는 건 요즘 제 취향이 아니라서, 그냥 제 기준으로만 정리할게요.

Bootstrap은 “버튼이 이렇게 생겼고, 카드가 이렇게 생겼다”까지 같이 주는 키트 느낌이 강해요. 문서 보고 복붙하면 빨리 나옵니다. 대신 우리 브랜드랑 1:1로 맞추려면 결국 오버라이드 싸움이 생기고, 그게 은근히 피곤해요.

Tailwind반찬 가게에 가까워요. 밑반찬(유틸)을 골라 담아서 한 끼(화면)를 만드는 거죠. 처음엔 “className이 왜 이렇게 길어?” 하다가, 막상 팀에서 색 이름·간격 스케일만 합의해 두면 수정 속도가 미친 듯이 붙는 경우가 많아요.

  • 번들: Tailwind는 최적화 잘 되면 가볍게 가는 편이고, Bootstrap은 “기본으로 딸려오는 게” 상대적으로 큰 느낌.
  • 학습: Bootstrap은 컴포넌트 이름 외우기, Tailwind는 유틸 이름·패턴에 익숙해지기. 저는 후자가 한번 몸에 붙으면 더 오래 갔어요.
  • JS 의존: Bootstrap 일부 UI는 JS가 필요하고, Tailwind 자체는 스타일만이면 됨(물론 프로젝트에 React 쓰면 그건 그거고).

완벽한 우열이 있다기보다, “키트로 빨리 vs 유틸로 맞춤” 트레이드오프예요. 저는 맞춤이 잦은 제품이면 Tailwind 쪽에 손이 더 갑니다.

유틸리티 퍼스트, 나는 이렇게 받아들였어요

입문기를 한 번 써볼게요. 첫 프로젝트에 Tailwind 넣었을 때 반응은 대략 이랬어요.

  1. 1주 차 — 혐오: div에 클래스가 한 줄을 가로질러요. “이게 맞나?” 싶었고, 예쁜 CSS 파일 대신 마크업이 비대해 보였어요.
  2. 2~3주 차 — 이탈 욕구 감소: CSS 파일 열었다 닫았다가 거의 사라졌어요. “이 버튼 hover 어디 있지?” 검색이 줄었죠.
  3. 한 달 넘어서 — 의존: 반응형 접두사(md:, lg:)랑 상태(hover:, dark:)가 손에 잡히기 시작하면, 디자인 수정이 “한 파일 안에서” 끝나는 경험이 나옵니다.

핵심은 “HTML이 지저분해 보인다”는 첫인상과, “수정 범위가 좁아진다”는 나중의 체감이 엇갈린다는 거예요. 저는 후자가 팀 작업일수록 커진다고 봐요.

설치 및 초기 설정

프레임워크마다 스크립트는 조금씩 달라요. 다만 “content에 스캔 경로 잡기 → CSS 진입점에 Tailwind 끼우기” 이 큰 그림은 같습니다. 아래는 자주 쓰는 뼈대만.

Vite + React

Vite + PostCSS 조합이면 대부분 별도 끔찍한 설정 없이 돌아가요. 로컬에서 npx tailwindcss init -p 한 번이면 tailwind.configpostcss.config가 생기고, 여기서 content에 “어디 파일을 뒤질지”만 잘 잡아 주면 돼요. 이거 빼먹으면 나중에 프로덕션에서 스타일 날아가는 클래식 케이스가 나옵니다.

# 프로젝트 생성
npm create vite@latest my-app -- --template react
cd my-app

# Tailwind 설치
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@tailwind base / components / utilities는 말 그대로 밑바닥·컴포넌트 레이어·유틸을 순서대로 넣는 거예요. 프로젝트마다 이걸 한 파일에 몰아넣을지 쪼갤지는 팀 취향이고요.

Next.js

# Next.js 13+ (자동 설치)
npx create-next-app@latest my-app
# Tailwind 사용 여부 선택: Yes

# 또는 기존 프로젝트에 추가
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Standalone CLI

# CDN (개발용만)
<script src="https://cdn.tailwindcss.com"></script>

# CLI로 컴파일
npx tailwindcss -i ./src/input.css -o ./dist/output.css --watch

기본 사용법

레이아웃

<!-- Flexbox -->
<div class="flex items-center justify-between">
  <div class="flex-1">Left</div>
  <div>Center</div>
  <div class="flex-1">Right</div>
</div>

<!-- Grid -->
<div class="grid grid-cols-3 gap-4">
  <div class="col-span-2">Main</div>
  <div>Sidebar</div>
</div>

<!-- Container -->
<div class="container mx-auto px-4">
  <div class="max-w-4xl mx-auto">
    Content
  </div>
</div>

타이포그래피

<!-- 텍스트 크기 -->
<h1 class="text-4xl font-bold">제목</h1>
<p class="text-base leading-relaxed">본문 텍스트</p>
<small class="text-sm text-gray-500">작은 텍스트</small>

<!-- 텍스트 정렬 -->
<p class="text-left">왼쪽</p>
<p class="text-center">가운데</p>
<p class="text-right">오른쪽</p>

<!-- 텍스트 색상 -->
<p class="text-gray-900">검은색</p>
<p class="text-blue-600">파란색</p>
<p class="text-red-500 hover:text-red-700">빨간색 (호버)</p>

<!-- 말줄임 -->
<p class="truncate">긴 텍스트는 잘립니다...</p>
<p class="line-clamp-3">3줄 이상은 잘립니다...</p>

스페이싱

<!-- Margin -->
<div class="m-4">전체 여백 1rem</div>
<div class="mt-8 mb-4">위 2rem, 아래 1rem</div>
<div class="mx-auto">가로 중앙</div>

<!-- Padding -->
<div class="p-4">전체 패딩 1rem</div>
<div class="px-6 py-3">가로 1.5rem, 세로 0.75rem</div>

<!-- Space Between -->
<div class="flex space-x-4">
  <div>아이템 1</div>
  <div>아이템 2</div>
  <div>아이템 3</div>
</div>

색상

<!-- 배경색 -->
<div class="bg-white">흰색</div>
<div class="bg-gray-100">밝은 회색</div>
<div class="bg-blue-500">파란색</div>
<div class="bg-gradient-to-r from-purple-400 to-pink-600">그라데이션</div>

<!-- 테두리 -->
<div class="border border-gray-300">테두리</div>
<div class="border-2 border-blue-500">두꺼운 파란 테두리</div>
<div class="border-t-4 border-red-500">위쪽만 두꺼운 빨간 테두리</div>

박스 스타일

<!-- 너비/높이 -->
<div class="w-64 h-32">고정 크기</div>
<div class="w-full h-screen">전체 크기</div>
<div class="w-1/2 h-auto">반 너비, 자동 높이</div>

<!-- 둥근 모서리 -->
<div class="rounded">기본 둥근 모서리</div>
<div class="rounded-lg">크게 둥근 모서리</div>
<div class="rounded-full">완전 둥근 (원)</div>

<!-- 그림자 -->
<div class="shadow">작은 그림자</div>
<div class="shadow-md">중간 그림자</div>
<div class="shadow-2xl">큰 그림자</div>
<div class="shadow-inner">안쪽 그림자</div>

반응형 디자인

<!-- 모바일 퍼스트 -->
<div class="text-sm md:text-base lg:text-lg xl:text-xl">
  반응형 텍스트 크기
</div>

<!-- 그리드 반응형 -->
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
  <div>아이템 1</div>
  <div>아이템 2</div>
  <div>아이템 3</div>
  <div>아이템 4</div>
</div>

<!-- 숨기기/보이기 -->
<div class="hidden md:block">태블릿 이상에서만 보임</div>
<div class="md:hidden">모바일에서만 보임</div>

<!-- Flexbox 방향 변경 -->
<div class="flex flex-col md:flex-row">
  <div class="w-full md:w-1/3">Sidebar</div>
  <div class="w-full md:w-2/3">Main</div>
</div>

브레이크포인트

// tailwind.config.js
module.exports = {
  theme: {
    screens: {
      'sm': '640px',   // @media (min-width: 640px)
      'md': '768px',   // @media (min-width: 768px)
      'lg': '1024px',  // @media (min-width: 1024px)
      'xl': '1280px',  // @media (min-width: 1280px)
      '2xl': '1536px', // @media (min-width: 1536px)
      
      // 커스텀 브레이크포인트
      'tablet': '640px',
      'laptop': '1024px',
      'desktop': '1280px',
    }
  }
}

상태 변형 (State Variants)

<!-- Hover -->
<button class="bg-blue-500 hover:bg-blue-700 text-white">
  Hover Me
</button>

<!-- Focus -->
<input class="border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200" />

<!-- Active -->
<button class="active:bg-blue-800">Click Me</button>

<!-- Disabled -->
<button class="disabled:opacity-50 disabled:cursor-not-allowed" disabled>
  Disabled
</button>

<!-- Group Hover -->
<div class="group">
  <img class="group-hover:opacity-75" src="image.jpg" />
  <p class="group-hover:text-blue-500">Title</p>
</div>

<!-- Peer (형제 요소 상태) -->
<input type="checkbox" class="peer" />
<label class="peer-checked:text-blue-500">체크되면 파란색</label>

<!-- Dark Mode -->
<div class="bg-white dark:bg-gray-800 text-black dark:text-white">
  다크모드 지원
</div>

다크모드 구현

// tailwind.config.js
module.exports = {
  darkMode: 'class', // 또는 'media' (시스템 설정)
  // ...
}
<!-- HTML에 class="dark" 추가 -->
<html class="dark">
  <body class="bg-white dark:bg-gray-900">
    <h1 class="text-black dark:text-white">제목</h1>
    <p class="text-gray-700 dark:text-gray-300">본문</p>
  </body>
</html>
// 다크모드 토글 (React)
import { useState, useEffect } from 'react';

function ThemeToggle() {
  const [darkMode, setDarkMode] = useState(false);

  useEffect(() => {
    if (darkMode) {
      document.documentElement.classList.add('dark');
    } else {
      document.documentElement.classList.remove('dark');
    }
  }, [darkMode]);

  return (
    <button
      onClick={() => setDarkMode(!darkMode)}
      className="p-2 rounded bg-gray-200 dark:bg-gray-700"
    >
      {darkMode ? '🌞' : '🌙'}
    </button>
  );
}

커스터마이징

색상 팔레트 확장

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        // 브랜드 컬러
        primary: {
          50: '#f0f9ff',
          100: '#e0f2fe',
          500: '#0ea5e9',
          600: '#0284c7',
          900: '#0c4a6e',
        },
        secondary: '#ff6b6b',
      }
    }
  }
}
<div class="bg-primary-500 text-white">Primary</div>
<button class="bg-secondary hover:bg-secondary/80">Secondary</button>

폰트 추가

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      fontFamily: {
        sans: ['Pretendard', 'Inter', 'sans-serif'],
        mono: ['Fira Code', 'monospace'],
        display: ['Playfair Display', 'serif'],
      }
    }
  }
}
<h1 class="font-display text-4xl">Display Font</h1>
<code class="font-mono">const x = 10;</code>

스페이싱 커스터마이징

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      spacing: {
        '128': '32rem',
        '144': '36rem',
      }
    }
  }
}

애니메이션

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      animation: {
        'fade-in': 'fadeIn 0.5s ease-in',
        'slide-up': 'slideUp 0.3s ease-out',
        'bounce-slow': 'bounce 3s infinite',
      },
      keyframes: {
        fadeIn: {
          '0%': { opacity: '0' },
          '100%': { opacity: '1' },
        },
        slideUp: {
          '0%': { transform: 'translateY(20px)', opacity: '0' },
          '100%': { transform: 'translateY(0)', opacity: '1' },
        }
      }
    }
  }
}
<div class="animate-fade-in">페이드 인</div>
<div class="animate-slide-up">슬라이드 업</div>
<div class="animate-bounce-slow">느린 바운스</div>

@apply로 클래스 재사용

/* src/styles/components.css */
@layer components {
  .btn {
    @apply px-4 py-2 rounded font-semibold transition-colors;
  }
  
  .btn-primary {
    @apply btn bg-blue-500 text-white hover:bg-blue-600;
  }
  
  .btn-secondary {
    @apply btn bg-gray-200 text-gray-800 hover:bg-gray-300;
  }
  
  .card {
    @apply p-6 bg-white rounded-lg shadow-md dark:bg-gray-800;
  }
  
  .input {
    @apply w-full px-4 py-2 border border-gray-300 rounded-md 
           focus:outline-none focus:ring-2 focus:ring-blue-500;
  }
}
<button class="btn-primary">Primary Button</button>
<div class="card">
  <h2>Card Title</h2>
  <p>Card content...</p>
</div>
<input type="text" class="input" placeholder="Enter text" />

플러그인 사용

공식 플러그인

# Typography - 산문 스타일
npm install -D @tailwindcss/typography

# Forms - 폼 요소 스타일 리셋
npm install -D @tailwindcss/forms

# Line Clamp - 여러 줄 말줄임
npm install -D @tailwindcss/line-clamp

# Aspect Ratio - 비율 유지
npm install -D @tailwindcss/aspect-ratio
// tailwind.config.js
module.exports = {
  plugins: [
    require('@tailwindcss/typography'),
    require('@tailwindcss/forms'),
    require('@tailwindcss/line-clamp'),
    require('@tailwindcss/aspect-ratio'),
  ],
}
<!-- Typography -->
<article class="prose lg:prose-xl dark:prose-invert">
  <h1>제목</h1>
  <p>마크다운 스타일 본문...</p>
</article>

<!-- Forms -->
<input type="email" class="form-input" />
<select class="form-select">...</select>

<!-- Line Clamp -->
<p class="line-clamp-3">긴 텍스트...</p>

<!-- Aspect Ratio -->
<div class="aspect-w-16 aspect-h-9">
  <iframe src="..."></iframe>
</div>

실전 컴포넌트 예제

카드 컴포넌트

<div class="max-w-sm rounded-lg overflow-hidden shadow-lg bg-white dark:bg-gray-800 transition-transform hover:scale-105">
  <img class="w-full h-48 object-cover" src="image.jpg" alt="Card" />
  <div class="p-6">
    <div class="flex items-center justify-between mb-2">
      <span class="text-sm text-gray-500 dark:text-gray-400">카테고리</span>
      <span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">New</span>
    </div>
    <h3 class="font-bold text-xl mb-2 text-gray-900 dark:text-white">
      카드 제목
    </h3>
    <p class="text-gray-700 dark:text-gray-300 text-base line-clamp-3">
      카드 설명 텍스트가 여기에 들어갑니다...
    </p>
    <div class="mt-4 flex items-center justify-between">
      <span class="text-sm text-gray-500">2024-04-18</span>
      <button class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors">
        자세히 보기
      </button>
    </div>
  </div>
</div>

내비게이션 바

<nav class="bg-white dark:bg-gray-800 shadow-lg sticky top-0 z-50">
  <div class="container mx-auto px-4">
    <div class="flex items-center justify-between h-16">
      <!-- 로고 -->
      <div class="flex items-center">
        <img src="logo.svg" alt="Logo" class="h-8 w-8" />
        <span class="ml-2 text-xl font-bold text-gray-900 dark:text-white">
          Brand
        </span>
      </div>
      
      <!-- 메뉴 -->
      <div class="hidden md:flex items-center space-x-8">
        <a href="#" class="text-gray-700 dark:text-gray-300 hover:text-blue-500 transition-colors">
          Home
        </a>
        <a href="#" class="text-gray-700 dark:text-gray-300 hover:text-blue-500 transition-colors">
          About
        </a>
        <a href="#" class="text-gray-700 dark:text-gray-300 hover:text-blue-500 transition-colors">
          Services
        </a>
        <a href="#" class="text-gray-700 dark:text-gray-300 hover:text-blue-500 transition-colors">
          Contact
        </a>
      </div>
      
      <!-- CTA 버튼 -->
      <div class="flex items-center space-x-4">
        <button class="px-4 py-2 text-blue-500 border border-blue-500 rounded hover:bg-blue-50 dark:hover:bg-gray-700 transition-colors">
          로그인
        </button>
        <button class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors">
          회원가입
        </button>
      </div>
    </div>
  </div>
</nav>

모달

<!-- 오버레이 -->
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50">
  <!-- 모달 -->
  <div class="bg-white dark:bg-gray-800 rounded-lg shadow-2xl max-w-md w-full animate-fade-in">
    <!-- 헤더 -->
    <div class="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
      <h3 class="text-xl font-bold text-gray-900 dark:text-white">
        모달 제목
      </h3>
      <button class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
        <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
        </svg>
      </button>
    </div>
    
    <!-- 본문 -->
    <div class="p-6">
      <p class="text-gray-700 dark:text-gray-300">
        모달 내용이 여기에 들어갑니다.
      </p>
    </div>
    
    <!-- 푸터 -->
    <div class="flex items-center justify-end gap-3 p-6 border-t border-gray-200 dark:border-gray-700">
      <button class="px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors">
        취소
      </button>
      <button class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors">
        확인
      </button>
    </div>
  </div>
</div>

폼 입력

<form class="max-w-lg mx-auto p-6 bg-white dark:bg-gray-800 rounded-lg shadow-md">
  <!-- 텍스트 입력 -->
  <div class="mb-4">
    <label class="block text-gray-700 dark:text-gray-300 text-sm font-bold mb-2">
      이메일
    </label>
    <input
      type="email"
      class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md 
             focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 
             dark:text-white"
      placeholder="[email protected]"
    />
  </div>
  
  <!-- 텍스트 영역 -->
  <div class="mb-4">
    <label class="block text-gray-700 dark:text-gray-300 text-sm font-bold mb-2">
      메시지
    </label>
    <textarea
      rows="4"
      class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md 
             focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 
             dark:text-white resize-none"
      placeholder="메시지를 입력하세요"
    ></textarea>
  </div>
  
  <!-- 체크박스 -->
  <div class="mb-6">
    <label class="flex items-center">
      <input type="checkbox" class="form-checkbox h-5 w-5 text-blue-500 rounded" />
      <span class="ml-2 text-gray-700 dark:text-gray-300">이용약관에 동의합니다</span>
    </label>
  </div>
  
  <!-- 제출 버튼 -->
  <button
    type="submit"
    class="w-full bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 
           focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 
           transition-colors"
  >
    제출하기
  </button>
</form>

성능 최적화

PurgeCSS (자동 최적화)

// tailwind.config.js
module.exports = {
  content: [
    './src/**/*.{js,jsx,ts,tsx}',
    './public/index.html',
  ],
  // Tailwind v3+는 자동으로 사용하지 않는 클래스 제거
}

JIT 모드 (기본 활성화)

// tailwind.config.js
module.exports = {
  mode: 'jit', // Tailwind v3부터 기본값
  // ...
}

프로덕션 빌드

// package.json
{
  "scripts": {
    "build:css": "tailwindcss -i ./src/input.css -o ./dist/output.css --minify"
  }
}

베스트 프랙티스

1. 일관된 스페이싱 사용

<!-- ✅ 좋음: 일관된 간격 -->
<div class="space-y-4">
  <div class="p-4">...</div>
  <div class="p-4">...</div>
</div>

<!-- ❌ 나쁨: 불규칙한 간격 -->
<div>
  <div class="p-3">...</div>
  <div class="p-5">...</div>
</div>

2. 컴포넌트 추출

// ✅ 좋음: 재사용 가능한 컴포넌트
function Button({ variant = 'primary', children, ...props }) {
  const baseClasses = 'px-4 py-2 rounded font-semibold transition-colors';
  const variantClasses = {
    primary: 'bg-blue-500 text-white hover:bg-blue-600',
    secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
  };
  
  return (
    <button className={`${baseClasses} ${variantClasses[variant]}`} {...props}>
      {children}
    </button>
  );
}

// ❌ 나쁨: 중복된 클래스
<button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
  Button 1
</button>
<button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
  Button 2
</button>

3. 의미있는 클래스 네임

/* ✅ 좋음 */
@layer components {
  .btn-primary { @apply px-4 py-2 bg-blue-500 text-white rounded; }
  .card-elevated { @apply p-6 bg-white rounded-lg shadow-lg; }
}

/* ❌ 나쁨 */
@layer components {
  .comp1 { @apply px-4 py-2 bg-blue-500; }
  .thing { @apply p-6 bg-white; }
}

VS Code 확장 및 도구

Tailwind CSS IntelliSense

// settings.json
{
  "tailwindCSS.experimental.classRegex": [
    ["clsx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"],
    ["className={([^}]*)", "'([^']*)'"]
  ]
}

Prettier Plugin

npm install -D prettier prettier-plugin-tailwindcss
// .prettierrc
{
  "plugins": ["prettier-plugin-tailwindcss"]
}

실전 사례: 반응형 대시보드 레이아웃

Tailwind의 진가는 복잡한 반응형 레이아웃을 구현할 때 드러납니다. 모바일, 태블릿, 데스크톱에서 각각 다른 레이아웃을 보여주는 대시보드를 만들어보겠습니다.

적응형 사이드바와 메인 콘텐츠

모바일에서는 사이드바가 숨겨지고 햄버거 메뉴로 표시되며, 태블릿 이상에서는 고정 사이드바로 표시됩니다. Tailwind의 반응형 유틸리티로 미디어 쿼리 없이 간단하게 구현할 수 있습니다.

function Dashboard() {
  const [sidebarOpen, setSidebarOpen] = useState(false);
  
  return (
    <div className="flex h-screen bg-gray-100">
      {/* 모바일 오버레이 */}
      {sidebarOpen && (
        <div 
          className="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden"
          onClick={() => setSidebarOpen(false)}
        />
      )}
      
      {/* 사이드바 */}
      <aside className={`
        fixed inset-y-0 left-0 z-50
        w-64 bg-white shadow-lg
        transform transition-transform duration-300 ease-in-out
        lg:translate-x-0 lg:static lg:z-0
        ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'}
      `}>
        <div className="flex items-center justify-between p-4 border-b">
          <h2 className="text-xl font-bold text-gray-800">Dashboard</h2>
          <button 
            className="lg:hidden p-2 rounded hover:bg-gray-100"
            onClick={() => setSidebarOpen(false)}
          >

          </button>
        </div>
        
        <nav className="p-4 space-y-2">
          <a href="#" className="block px-4 py-2 rounded hover:bg-gray-100">

          </a>
          <a href="#" className="block px-4 py-2 rounded hover:bg-gray-100">
            통계
          </a>
          <a href="#" className="block px-4 py-2 rounded hover:bg-gray-100">
            설정
          </a>
        </nav>
      </aside>
      
      {/* 메인 콘텐츠 */}
      <main className="flex-1 overflow-auto">
        {/* 헤더 */}
        <header className="bg-white shadow-sm sticky top-0 z-10">
          <div className="flex items-center justify-between p-4">
            <button 
              className="lg:hidden p-2 rounded hover:bg-gray-100"
              onClick={() => setSidebarOpen(true)}
            >

            </button>
            <h1 className="text-2xl font-bold text-gray-800">대시보드</h1>
            <div className="flex items-center gap-4">
              <button className="p-2 rounded-full hover:bg-gray-100">
                🔔
              </button>
              <img 
                src="/avatar.jpg" 
                alt="Profile"
                className="w-10 h-10 rounded-full"
              />
            </div>
          </div>
        </header>
        
        {/* 콘텐츠 그리드 */}
        <div className="p-4 lg:p-6">
          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 lg:gap-6">
            {/* 카드들 */}
            {[1, 2, 3, 4, 5, 6].map((i) => (
              <div 
                key={i}
                className="bg-white rounded-lg shadow p-6 hover:shadow-lg transition-shadow"
              >
                <h3 className="text-lg font-semibold mb-2">통계 {i}</h3>
                <p className="text-3xl font-bold text-blue-600">1,234</p>
                <p className="text-sm text-gray-500 mt-2">전월 대비 +12%</p>
              </div>
            ))}
          </div>
        </div>
      </main>
    </div>
  );
}

이 코드는 단 몇 줄의 클래스로 완전한 반응형 대시보드를 구현합니다. CSS 파일을 오가며 미디어 쿼리를 작성할 필요가 없습니다.

디자인 시스템 구축

팀 전체가 일관된 스타일을 사용하려면 디자인 토큰을 설정 파일에 정의해야 합니다. Tailwind 설정을 확장하여 프로젝트만의 색상, 간격, 폰트를 정의할 수 있습니다.

// tailwind.config.js
export default {
  theme: {
    extend: {
      colors: {
        primary: {
          50: '#eff6ff',
          100: '#dbeafe',
          500: '#3b82f6',
          600: '#2563eb',
          900: '#1e3a8a',
        },
        brand: {
          red: '#ef4444',
          green: '#10b981',
        },
      },
      spacing: {
        '18': '4.5rem',
        '88': '22rem',
      },
      fontFamily: {
        sans: ['Pretendard', 'system-ui', 'sans-serif'],
        mono: ['JetBrains Mono', 'monospace'],
      },
      borderRadius: {
        '4xl': '2rem',
      },
    },
  },
}

이제 bg-primary-500, text-brand-red, rounded-4xl 같은 커스텀 클래스를 사용할 수 있습니다.

트러블슈팅

스타일이 적용되지 않을 때

가장 흔한 문제는 Tailwind가 파일을 스캔하지 못하는 것입니다. content 배열에 올바른 경로가 포함되어야 하며, 동적으로 클래스 이름을 생성하면 Tailwind가 감지하지 못합니다.

// 1. content 경로 확인
module.exports = {
  content: [
    "./src/**/*.{js,jsx,ts,tsx}", // 올바른 경로
  ],
}

// 2. 동적 클래스는 피하기
// ❌ 작동 안 함 (Tailwind가 text-red-500을 찾을 수 없음)
<div className={`text-${color}-500`}>

// ✅ 작동함 (전체 클래스 이름이 코드에 존재)
<div className={color === 'blue' ? 'text-blue-500' : 'text-red-500'}>

다크모드가 작동하지 않을 때

다크모드를 토글하려면 ‘class’ 전략을 사용하고, JavaScript로 HTML 요소에 dark 클래스를 추가/제거해야 합니다.

// tailwind.config.js
module.exports = {
  darkMode: 'class', // 'media' 대신 'class' 사용
  // ...
}
// 다크모드 토글
function toggleDarkMode() {
  document.documentElement.classList.toggle('dark');
}

빌드 크기가 큰 경우

프로덕션 빌드에서 번들 크기가 예상보다 크다면, content 경로가 너무 넓거나 PurgeCSS가 제대로 작동하지 않는 것입니다.

// 불필요한 파일 제외
module.exports = {
  content: [
    "./src/**/*.{js,jsx,ts,tsx}",
    "!./src/**/*.spec.{js,jsx,ts,tsx}", // 테스트 파일 제외
  ],
}

삽질하면서 배운 것 (개인 노트)

여기부터는 제 경험담이에요. 팀마다 다르니까 “절대 규칙”은 아닙니다.

  • 동적 클래스 문자열은 함정이에요. `text-${color}-500` 같이 조합하면 빌드가 못 본 척하고 날려버릴 수 있어요. 색이 몇 개 없으면 삼항이나 맵으로 풀 클래스 이름을 박아 두는 편이 마음 편했어요.
  • tailwind-merge는 한번 써볼 만해요. 조건부로 className을 붙이다 보면 충돌 나는데, 이걸로 “마지막에 이긴 유틸”을 정리하기 쉬워요.
  • @apply는 도구일 뿐이에요. 전부 @apply로 빼면 결국 예전 CSS와 다를 바 없이 덩어리가 되고, 반대로 유틸만 쌓으면 컴포넌트 경계가 지저분해져요. 저는 “반복되는 버튼·인풋만 승격” 정도로 씁니다.
  • content 경로 빼먹으면 프로덕션만 터져요. 스토리북·패키지 UI 경로가 빠진 모노레포에서 특히 자주 봤어요. CI에서 한번 스캔 결과 CSS 용량 찍어보는 것도 괜찮아요.

주의사항

1. 클래스 순서에 의존하지 마세요

Tailwind는 “HTML에 적은 순서”로 이기는 게 아니에요. bg-red-500 bg-blue-500 같이 올리면 생성된 CSS에서 누가 이기는지로 갈립니다. 조건부 UI면 tailwind-merge나 variant를 쓰는 쪽이 덜 스트레스예요.

2. 너무 많은 @apply 사용 피하기

@apply만 늘리면 “유틸로 빠르게”는 점점 사라져요. 반복 패턴은 컴포넌트로 빼고, 정말 공통화할 것만 @apply — 이 균형이 저는 제일 어렵더라고요.

3. 프로덕션 빌드 체인

개발/프로덕션에서 스캔·최적화 경로가 다르면 “로컬에선 되는데 배포만 이상한” 그림이 나와요. 빌드 스크립트랑 content를 팀에서 한번 고정해 두는 걸 추천해요.

정리하면, Tailwind는 처음엔 지저분해 보이는데 익숙해지면 수정 속도와 합의 비용에서 이득이 나는 경우가 많아요. 저는 그 트레이드오프를 감수할만하다고 봅니다. 아래 예제·트러블슈팅은 그대로 두었으니, 필요한 부분만 집어 쓰세요.


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 새 화면을 자주 찍어내거나, 디자인 시스템을 코드에 박아 넣어야 할 때요. 위에 설치·유틸 예제·트러블슈팅은 그대로 복붙해서 실험해 보고, 팀 규칙(토큰, merge, content)만 맞추면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 아래 같이 보면 좋은 글 링크나, 이 블로그의 Tailwind·PostCSS 쪽 글을 같이 보면 흐름이 잡혀요. 한 글에 모든 게 다 있을 순 없으니, 막힐 때만 골라 읽어도 충분해요.

Q. 더 깊이 공부하려면?

A. Tailwind 공식 문서가 제일 빠르고요. v3/v4 전환 중이면 팀 빌드 체인(Vite·Next·Astro 등) 문서도 같이 보는 걸 추천해요.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

Tailwind CSS, CSS, Frontend, UI, Styling, Design System, Responsive Design 등으로 검색하시면 이 글이 도움이 됩니다.