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

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

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

이 글의 핵심

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

Tailwind CSS란?

Tailwind CSS는 미리 정의된 클래스를 조합해 HTML(또는 JSX) 안에서 직접 스타일을 적용하는 유틸리티 우선(Utility-First) CSS 프레임워크입니다. flex, p-4, text-lg처럼 각 클래스가 하나의 CSS 속성 역할만 수행하며, 이런 클래스들을 여러 개 조합해서 원하는 디자인을 완성합니다.

기존 CSS 작업 방식은 별도의 .css 파일에 클래스를 정의하고, 마크업에서는 그 클래스 이름만 참조하는 구조였습니다. 이 방식은 스타일과 마크업이 분리되어 있어 “이 버튼의 호버 스타일이 어디에 정의되어 있는지” 찾아다녀야 하는 컨텍스트 스위칭 비용이 발생합니다. Tailwind는 스타일 정의를 마크업 안으로 가져와 이 탐색 비용을 줄이는 데 초점을 맞춘 프레임워크입니다.

핵심 특징

  1. 유틸리티 우선(Utility-First) 클래스 하나가 하나의 역할만 수행하도록 설계되어 있습니다. margin-top: 2rem을 적용하려면 별도의 커스텀 클래스를 만드는 대신 mt-8을 바로 사용합니다. 스타일을 추상화된 의미 단위가 아니라, 작은 유틸리티의 조합으로 쌓아 올리는 방식입니다.

  2. JIT(Just-In-Time) 컴파일러 Tailwind v3부터는 실제로 마크업에서 사용된 클래스만 스캔해서 그 순간 CSS를 생성합니다. 개발 서버 구동 시점에 전체 유틸리티 세트를 미리 만들어 두지 않기 때문에 빌드가 빠르고, 프로덕션 번들에는 실제로 쓰인 클래스만 남습니다. 다만 이 최적화는 클래스 이름이 소스 코드에 완전한 문자열로 존재할 때만 정상 동작하며, 문자열을 동적으로 조합하면 스캐너가 감지하지 못한다는 제약이 있습니다. 이 부분은 아래 트러블슈팅 섹션에서 자세히 다룹니다.

  3. 커스터마이징 tailwind.config.js(또는 v4의 CSS 기반 @theme)를 통해 팀에서 사용할 색상, 간격, 폰트 스케일을 프로젝트 차원에서 고정할 수 있습니다. 기본 제공되는 유틸리티만으로는 부족한 브랜드 디자인 시스템을 코드베이스에 직접 반영하는 용도로 활용합니다.

  4. 최신 CSS 기능 지원 Grid, Flexbox, CSS 커스텀 속성, Container Queries 같은 최신 레이아웃 기능을 별도의 문법 학습 없이 유틸리티 클래스 형태로 사용할 수 있습니다.

Bootstrap과의 차이점

Tailwind와 Bootstrap은 둘 다 CSS 프레임워크지만 접근 방식이 근본적으로 다릅니다. 어떤 프레임워크가 프로젝트에 더 적합한지는 팀의 작업 방식과 디자인 요구사항에 따라 달라집니다.

Bootstrap은 버튼, 카드, 내비게이션 바처럼 완성된 형태의 컴포넌트를 미리 제공하는 프레임워크입니다. 공식 문서에서 원하는 컴포넌트를 찾아 클래스를 그대로 붙여 넣으면 빠르게 화면을 구성할 수 있습니다. 다만 프로젝트 고유의 브랜드 디자인에 맞추려면 기본 스타일을 오버라이드하는 작업이 추가로 필요하며, 커스터마이징 범위가 커질수록 오버라이드 코드가 늘어나는 경향이 있습니다.

Tailwind는 완성된 컴포넌트 대신 작은 단위의 유틸리티 클래스를 제공합니다. 이 유틸리티를 조합해서 원하는 디자인을 직접 만드는 구조이기 때문에, 초기에는 className이 길어 보일 수 있지만 팀에서 색상·간격 스케일에 대한 합의만 이루어지면 이후 수정 속도가 빨라지는 경우가 많습니다.

두 프레임워크의 실무적인 차이를 정리하면 다음과 같습니다.

  • 번들 크기: Tailwind는 사용한 클래스만 최종 CSS에 남기 때문에 최적화가 잘 되면 번들이 가벼운 편입니다. Bootstrap은 컴포넌트 전체 스타일이 기본으로 포함되어 상대적으로 무겁습니다.
  • 학습 곡선: Bootstrap은 컴포넌트 이름과 구조를 익히면 되고, Tailwind는 유틸리티 클래스 이름과 조합 패턴에 익숙해져야 합니다. 초기 학습 비용은 Tailwind 쪽이 조금 더 있지만, 한번 익숙해지면 새 디자인에 대응하는 속도가 빨라지는 편입니다.
  • 자바스크립트 의존성: Bootstrap의 드롭다운, 모달 같은 일부 컴포넌트는 자체 JS가 필요합니다. Tailwind 자체는 스타일링 라이브러리이므로 상호작용 로직은 React, Vue 같은 프레임워크나 별도 라이브러리(Headless UI 등)에 맡깁니다.

어느 쪽이 절대적으로 우수하다기보다, “완성된 컴포넌트로 빠르게 시작할지” 또는 “유틸리티로 세밀하게 맞출지”의 트레이드오프에 가깝습니다. 브랜드 디자인 커스터마이징이 자주 필요한 프로젝트라면 Tailwind가, 프로토타입을 빠르게 만들어야 하는 상황이라면 Bootstrap이 더 유리할 수 있습니다.

유틸리티 우선 방식에 적응하는 과정

Tailwind를 처음 도입하는 팀에서는 보통 다음과 같은 단계를 거쳐 적응합니다.

  1. 도입 초기: div 태그 하나에 클래스가 한 줄을 가득 채우는 모습을 보고 거부감을 느끼는 경우가 많습니다. 별도 CSS 파일에 깔끔하게 정리된 클래스 대신 마크업 자체가 길어 보이기 때문입니다.
  2. 적응 단계: CSS 파일을 오가며 특정 스타일을 찾는 빈도가 줄어듭니다. “이 버튼의 호버 스타일이 어디 있지?”라는 질문 대신 해당 엘리먼트의 클래스만 확인하면 되기 때문입니다.
  3. 숙련 단계: 반응형 접두사(md:, lg:)와 상태 변형(hover:, dark:)이 손에 익으면, 디자인 수정이 하나의 파일 안에서 끝나는 경험을 하게 됩니다. CSS 파일과 마크업 파일을 오가는 시간이 크게 줄어듭니다.

“HTML이 지저분해 보인다”는 첫인상과 “수정 범위가 좁아진다”는 실제 체감이 서로 다른 방향을 가리키는 경우가 많습니다. 특히 여러 명이 함께 작업하는 팀 프로젝트일수록 후자의 이점이 두드러지는 경향이 있습니다.

설치 및 초기 설정

빌드 도구마다 설치 스크립트는 조금씩 다르지만, 기본 흐름은 동일합니다. Tailwind에게 “어떤 파일을 스캔해서 사용된 클래스를 찾을지”(content 경로)를 알려주고, CSS 진입점에 Tailwind 지시어를 추가하는 두 단계로 요약됩니다.

Vite + React

Vite와 PostCSS 조합에서는 별도의 복잡한 설정 없이 빠르게 시작할 수 있습니다. npx tailwindcss init -p 명령을 실행하면 tailwind.config.jspostcss.config.js가 생성되며, 이 중 tailwind.config.jscontent 배열에 실제 프로젝트 파일 경로를 정확히 지정하는 것이 중요합니다. 이 경로 설정을 빼먹으면 개발 환경에서는 정상 작동하다가 프로덕션 빌드에서만 스타일이 통째로 사라지는 문제가 자주 발생합니다.

# 프로젝트 생성
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, @tailwind components, @tailwind utilities는 각각 브라우저 기본 스타일을 초기화하는 레이어, 컴포넌트 클래스가 들어갈 레이어, 유틸리티 클래스가 들어갈 레이어를 순서대로 CSS에 주입합니다. 이 순서를 바꾸면 CSS 우선순위 계산이 꼬일 수 있으므로 그대로 유지하는 것을 권장합니다.

Next.js

Next.js 13 이상에서는 create-next-app 실행 시 Tailwind 사용 여부를 선택할 수 있어 별도 설정 없이 바로 시작할 수 있습니다. 기존 프로젝트에 추가하는 경우에도 Vite와 동일한 패키지를 설치하고 초기화 명령을 실행하면 됩니다.

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

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

빌드 도구 없이 사용하기

빠른 프로토타이핑이나 학습 목적이라면 CDN 스크립트로 Tailwind를 즉시 사용할 수 있습니다. 다만 이 방식은 런타임에 CSS를 생성하므로 성능이 떨어지고 커스터마이징 범위도 제한적이라, 프로덕션 서비스에는 권장하지 않습니다. 실제 서비스에서는 CLI로 정적 CSS 파일을 미리 컴파일해서 사용합니다.

# CDN (개발/프로토타입 전용, 프로덕션에는 부적합)
<script src="https://cdn.tailwindcss.com"></script>

# CLI로 컴파일 (프로덕션 권장 방식)
npx tailwindcss -i ./src/input.css -o ./dist/output.css --watch

기본 사용법

Tailwind의 유틸리티 클래스는 CSS 속성과 거의 1:1로 대응되도록 설계되어 있어, CSS 문법을 알고 있다면 클래스 이름만 보고도 어떤 스타일이 적용될지 예측할 수 있습니다. 아래는 실무에서 가장 자주 쓰이는 카테고리별 예제입니다.

레이아웃

Flexbox와 Grid는 Tailwind에서 각각 flex, grid 클래스로 시작합니다. 자식 요소의 정렬이나 크기 비율은 items-*, justify-*, col-span-* 같은 보조 클래스로 제어합니다.

<!-- 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>

첫 번째 예제는 justify-between으로 좌우 요소를 양 끝에 배치하고 가운데 요소는 남은 공간에 자연스럽게 위치시킵니다. 두 번째 예제는 3열 그리드에서 col-span-2를 사용해 특정 셀이 2칸을 차지하도록 지정한 것으로, 메인 콘텐츠와 사이드바 레이아웃을 구성할 때 자주 쓰이는 패턴입니다. container mx-auto는 화면 크기에 따라 최대 너비를 제한하고 좌우 여백을 자동으로 맞춰주는 조합으로, 페이지 전체 레이아웃의 기준선을 잡을 때 사용합니다.

타이포그래피

텍스트 크기, 정렬, 색상, 말줄임 처리를 각각 별도의 클래스로 제어합니다.

<!-- 텍스트 크기 -->
<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>

truncate는 한 줄을 넘어가는 텍스트를 말줄임표로 잘라내고, line-clamp-3은 지정한 줄 수를 초과하는 내용을 자동으로 잘라줍니다. 카드 컴포넌트의 요약문처럼 텍스트 길이가 유동적인 영역에서 레이아웃이 깨지지 않도록 할 때 유용합니다.

스페이싱

여백은 m(margin)과 p(padding) 접두사에 방향(t, b, l, r, x, y)과 숫자 스케일을 조합해서 지정합니다. 숫자 하나는 0.25rem 단위이므로 p-41rem(16px)에 해당합니다.

<!-- 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>

space-x-4는 형제 요소 사이에만 여백을 넣어주는 유틸리티로, 각 아이템에 개별적으로 마진을 지정하지 않아도 일정한 간격을 유지할 수 있습니다. 리스트나 버튼 그룹처럼 반복되는 요소를 나열할 때 특히 유용합니다.

색상과 테두리

배경색과 테두리는 Tailwind가 제공하는 색상 팔레트(gray, blue, red 등)와 명도 단계(50~900)를 조합해서 지정합니다.

<!-- 배경색 -->
<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>

bg-gradient-to-r은 왼쪽에서 오른쪽으로 흐르는 그라데이션을 생성하며, from-*to-*로 시작 색상과 끝 색상을 지정합니다. 테두리는 border-t-4처럼 특정 방향만 두께를 지정할 수 있어, 카드나 알림 컴포넌트의 강조선을 표현할 때 자주 사용합니다.

박스 스타일

크기, 모서리 둥글기, 그림자는 각각 w/h, rounded, shadow 접두사로 제어합니다.

<!-- 너비/높이 -->
<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>

h-screen은 뷰포트 높이(100vh)를 그대로 사용하는 클래스로, 첫 화면을 꽉 채우는 히어로 섹션을 만들 때 자주 쓰입니다. 그림자는 shadow부터 shadow-2xl까지 단계별로 제공되며, 숫자가 커질수록 그림자의 번짐과 깊이가 커집니다.

반응형 디자인

Tailwind는 별도의 미디어 쿼리 문법을 작성할 필요 없이, 클래스 앞에 sm:, md:, lg:, xl:, 2xl: 접두사를 붙이는 것만으로 반응형 스타일을 적용합니다. 기본값은 모바일 화면을 기준으로 하고, 접두사가 붙은 클래스는 해당 크기 이상의 화면에서만 적용되는 모바일 퍼스트(Mobile-First) 방식입니다.

<!-- 모바일 퍼스트 -->
<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>

위 그리드 예제는 화면이 좁을 때는 1열, 태블릿 이상에서는 2열, 데스크톱에서는 3~4열로 자동 전환됩니다. flex-col md:flex-row처럼 방향 자체를 반응형으로 바꾸는 패턴은 모바일에서는 세로로 쌓이던 사이드바와 본문이 데스크톱에서는 가로로 나란히 배치되는 레이아웃을 구현할 때 유용합니다.

브레이크포인트 커스터마이징

기본 브레이크포인트로 충분하지 않다면 tailwind.config.jsscreens에서 직접 값을 재정의하거나 이름을 추가할 수 있습니다.

// 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',
    }
  }
}

screens 객체를 통째로 재정의하면 기존 sm, md 등이 사라지므로, 기본값을 유지하면서 추가하려면 theme.extend.screens를 사용하는 것이 안전합니다. 디자인 시스템에서 tablet, laptop처럼 의미가 드러나는 이름을 함께 정의해두면 팀원 간 커뮤니케이션이 쉬워집니다.

상태 변형(State Variants)

hover:, focus:, active:, disabled: 같은 접두사는 요소의 상태에 따라 다른 스타일을 적용합니다. 별도의 CSS 의사 클래스(pseudo-class) 문법을 작성하지 않고도 인터랙션 스타일을 클래스만으로 표현할 수 있습니다.

<!-- 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>

groupgroup-hover:는 부모 요소에 마우스를 올렸을 때 자식 요소의 스타일을 함께 바꾸는 패턴입니다. 카드 전체에 호버했을 때 내부 이미지와 제목 색상이 동시에 변하는 UI를 구현할 때 사용합니다. peerpeer-checked:는 반대로 형제 요소의 상태(체크박스가 체크되었는지 등)를 기준으로 다른 형제 요소의 스타일을 바꿀 때 사용하며, 커스텀 체크박스나 토글 UI를 순수 CSS만으로 구현할 수 있게 해줍니다.

다크모드 구현

Tailwind의 다크모드는 darkMode 설정값에 따라 두 가지 전략 중 하나로 동작합니다. 'media'는 운영체제의 다크모드 설정을 그대로 따르고, 'class'는 특정 요소에 dark 클래스가 붙어 있을 때만 다크 스타일을 적용합니다. 사용자가 직접 테마를 전환할 수 있는 토글 버튼을 만들려면 'class' 전략을 사용해야 합니다.

// 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>

dark: 접두사가 붙은 클래스는 조상 요소 중 하나에 dark 클래스가 있을 때만 활성화됩니다. 아래 예제처럼 자바스크립트로 document.documentElement의 클래스를 토글하면, 새로고침 없이 즉시 테마를 전환할 수 있습니다.

// 다크모드 토글 (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>
  );
}

이 컴포넌트는 darkMode 상태가 바뀔 때마다 useEffect에서 루트 엘리먼트의 클래스를 add/remove로 전환합니다. 실제 서비스에서는 여기에 localStorage 저장 로직을 추가해 사용자가 선택한 테마를 새로고침 후에도 유지하는 경우가 많습니다.

커스터마이징

Tailwind의 기본 디자인 토큰(색상, 폰트, 간격 등)은 대부분의 프로젝트에 충분하지만, 브랜드 고유의 디자인 시스템을 반영하려면 tailwind.config.jstheme.extend를 통해 값을 확장합니다. extend를 사용하면 기본값을 유지하면서 새로운 값만 추가할 수 있어, theme를 직접 덮어쓰는 것보다 안전합니다.

색상 팔레트 확장

// 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>

색상을 50부터 900까지 명도 단계별로 정의해두면 bg-primary-500, text-primary-900처럼 기존 회색조 팔레트와 동일한 방식으로 사용할 수 있습니다. hover:bg-secondary/80/80은 투명도를 80%로 지정하는 문법으로, 별도의 색상 값을 추가하지 않고도 호버 시 살짝 투명해지는 효과를 표현할 수 있습니다.

폰트 추가

// 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>

fontFamily에 배열로 값을 지정하면 첫 번째 폰트가 로드되지 않았을 때 순서대로 대체 폰트를 사용하는 폴백 체인이 구성됩니다. 웹 폰트는 별도로 @font-face 또는 <link> 태그로 로드해야 하며, Tailwind 설정은 로드된 폰트를 어떤 클래스 이름으로 참조할지만 정의한다는 점에 유의해야 합니다.

스페이싱 커스터마이징

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

기본 스페이싱 스케일에 없는 큰 값이 필요할 때(예: 풀스크린 섹션의 최소 높이) 이렇게 숫자 키를 추가하면 w-128, h-144 같은 클래스를 바로 사용할 수 있습니다.

애니메이션

// 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>

keyframes에서 애니메이션의 각 단계를 정의하고, animation에서 그 keyframe 이름과 지속 시간, 이징(easing), 반복 여부를 조합해 새로운 애니메이션 유틸리티를 만듭니다. 이렇게 등록한 애니메이션은 animate-fade-in처럼 일반 유틸리티 클래스와 동일한 방식으로 사용할 수 있습니다.

@apply로 클래스 재사용

같은 유틸리티 조합이 여러 곳에서 반복된다면 @apply 지시어로 별도의 CSS 클래스에 묶어낼 수 있습니다. 버튼, 카드, 입력 필드처럼 프로젝트 전반에서 반복적으로 등장하는 컴포넌트 스타일에 적합합니다.

/* 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" />

@layer components로 감싸는 이유는 CSS 우선순위 때문입니다. Tailwind는 base, components, utilities 순서로 CSS를 생성하므로, @apply로 만든 컴포넌트 클래스보다 나중에 오는 유틸리티 클래스가 항상 더 높은 우선순위를 갖게 됩니다. 즉 <button class="btn-primary bg-red-500">처럼 나중에 개별 유틸리티로 덮어쓰는 것이 항상 가능합니다. 다만 @apply를 지나치게 많이 사용하면 결국 일반 CSS 파일과 다를 바 없는 구조로 돌아가게 되므로, 실제로 여러 곳에서 반복되는 패턴에만 제한적으로 사용하는 것을 권장합니다.

플러그인 사용

Tailwind 팀에서 공식적으로 제공하는 플러그인을 사용하면 기본 유틸리티만으로 다루기 까다로운 영역(긴 본문 텍스트, 폼 요소 초기화 등)을 손쉽게 처리할 수 있습니다.

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

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

# Aspect Ratio - 비율 유지 (Tailwind v3.0+는 기본 내장)
npm install -D @tailwindcss/aspect-ratio
// tailwind.config.js
module.exports = {
  plugins: [
    require('@tailwindcss/typography'),
    require('@tailwindcss/forms'),
    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>

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

@tailwindcss/typographyprose 클래스는 마크다운이나 CMS에서 가져온 HTML처럼 개별 태그에 클래스를 붙이기 어려운 콘텐츠에 제목, 문단, 리스트, 인용문 스타일을 일괄 적용합니다. 블로그 본문이나 문서 페이지에 자주 사용됩니다. @tailwindcss/forms는 브라우저마다 다르게 렌더링되는 input, select, checkbox 등의 기본 스타일을 초기화해서 커스터마이징하기 쉬운 상태로 맞춰줍니다.

실전 컴포넌트 예제

지금까지 살펴본 유틸리티를 조합하면 실무에서 바로 쓸 수 있는 컴포넌트를 구성할 수 있습니다. 아래 네 가지는 대부분의 서비스에서 공통적으로 필요한 UI 패턴입니다.

카드 컴포넌트

<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">2026-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>

hover:scale-105transition-transform을 함께 사용하면 마우스를 올렸을 때 카드가 부드럽게 확대되는 효과가 적용됩니다. object-cover는 이미지 비율이 컨테이너와 다르더라도 잘림 없이 영역을 꽉 채우도록 처리하는 속성으로, 다양한 크기의 이미지를 동일한 카드 레이아웃에 표시할 때 필수적입니다.

내비게이션 바

<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>

sticky top-0 z-50은 스크롤이 내려가도 내비게이션 바가 화면 상단에 고정되도록 만드는 조합입니다. hidden md:flex는 모바일 화면에서는 메뉴를 숨기고 태블릿 이상에서만 가로 메뉴를 보여주는 패턴으로, 실무에서는 이 자리에 모바일 전용 햄버거 메뉴 버튼을 별도로 배치하는 경우가 많습니다.

모달

<!-- 오버레이 -->
<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>

fixed inset-0으로 화면 전체를 덮는 오버레이를 만들고, bg-black/50 backdrop-blur-sm으로 반투명 배경과 블러 효과를 함께 적용해 뒤쪽 콘텐츠와 모달을 시각적으로 분리합니다. 이 마크업은 스타일만 담당하며, 실제 서비스에서는 Escape 키 처리나 포커스 트랩 같은 접근성 로직을 자바스크립트로 별도 구현해야 한다는 점에 유의해야 합니다.

폼 입력

<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@example.com"
    />
  </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>

폼 요소는 focus:ring-2focus:ring-offset-2를 조합해 키보드 포커스가 명확히 드러나도록 처리하는 것이 접근성 측면에서 중요합니다. focus:outline-none으로 브라우저 기본 아웃라인을 제거했다면, 반드시 focus:ring-* 같은 대체 시각 효과를 넣어 포커스 상태를 시각적으로 확인할 수 있도록 해야 합니다.

성능 최적화

Tailwind v3 이상에서는 프로덕션 빌드 시 content 배열에 지정된 파일들을 스캔해서 실제로 사용된 클래스만 최종 CSS에 남깁니다. 이 과정을 통해 수천 개에 달하는 전체 유틸리티 세트 중 실제 사용되는 일부만 번들에 포함되므로, 최종 CSS 크기가 크게 줄어듭니다.

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

JIT 엔진은 Tailwind v3부터 기본으로 활성화되어 있으므로 별도의 mode: 'jit' 설정이 필요하지 않습니다. 프로덕션 빌드는 CLI의 --minify 옵션으로 공백과 주석을 제거해 파일 크기를 추가로 줄일 수 있습니다.

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

content 경로 설정이 실제 사용 파일보다 좁으면 특정 페이지에서만 스타일이 누락되는 문제가 생기고, 반대로 너무 넓으면 스캔 대상이 불필요하게 늘어나 빌드 시간이 길어질 수 있습니다. 모노레포 환경에서는 공용 UI 패키지나 스토리북 파일 경로가 content 배열에서 누락되기 쉬우므로 특히 주의가 필요합니다.

베스트 프랙티스

1. 일관된 스페이싱 사용

같은 화면 안에서 p-3p-5처럼 인접한 스케일 값을 섞어 쓰면 디자인이 미세하게 어긋나 보입니다. 팀에서 사용할 간격 스케일을 미리 합의해두고 일관되게 적용하는 것이 좋습니다.

<!-- 좋은 예: 일관된 간격 -->
<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. 컴포넌트 추출

같은 클래스 조합이 여러 위치에서 반복된다면 React 같은 컴포넌트 기반 프레임워크에서는 함수 컴포넌트로, 순수 HTML 프로젝트에서는 @apply로 추출하는 것이 유지보수에 유리합니다.

// 좋은 예: 재사용 가능한 컴포넌트
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. 의미 있는 클래스 이름

@apply로 컴포넌트 클래스를 만들 때는 comp1, thing처럼 의미 없는 이름 대신 역할이 드러나는 이름을 사용해야 합니다.

/* 좋은 예 */
@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

공식 VS Code 확장인 Tailwind CSS IntelliSense는 클래스 이름 자동완성과 실제 CSS 값 미리보기를 제공합니다. clsxcva 같은 헬퍼 함수 안에서 작성한 클래스도 인식하도록 하려면 classRegex 설정을 추가해야 합니다.

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

Prettier Plugin

prettier-plugin-tailwindcss는 클래스 이름의 순서를 Tailwind가 권장하는 순서(레이아웃 → 스페이싱 → 타이포그래피 → 색상 순)로 자동 정렬해줍니다. 클래스 순서 자체는 최종 스타일에 영향을 주지 않지만, 팀 전체가 동일한 순서 규칙을 따르면 코드 리뷰에서 diff를 읽기가 훨씬 수월해집니다.

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>
  );
}

이 컴포넌트에서 핵심은 lg:translate-x-0 lg:static입니다. 데스크톱 화면(lg 이상)에서는 사이드바가 항상 제자리에 고정되어 보이고, 그보다 좁은 화면에서는 sidebarOpen 상태에 따라 translate-x-0(화면 안으로)과 -translate-x-full(화면 밖으로) 사이를 전환합니다. transition-transform duration-300이 붙어 있어 이 전환이 애니메이션으로 부드럽게 처리됩니다. 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 배열에 올바른 경로가 포함되어 있는지 먼저 확인해야 하며, 클래스 이름을 문자열 템플릿으로 동적 생성하면 JIT 스캐너가 이를 감지하지 못합니다.

// 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'}>

JIT 스캐너는 정규식 기반으로 소스 파일 전체를 훑어 클래스 이름 “문자열”을 찾기 때문에, 변수로 조합된 클래스는 빌드 시점에 완성된 문자열로 존재하지 않아 최종 CSS에서 누락됩니다. 색상 값이 몇 가지로 제한되어 있다면 삼항 연산자나 매핑 객체로 완전한 클래스 이름을 코드에 명시하는 방식으로 우회해야 합니다.

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

다크모드를 사용자가 직접 토글할 수 있게 하려면 darkMode 설정을 'class'로 지정하고, 자바스크립트로 HTML 요소에 dark 클래스를 추가하거나 제거해야 합니다. 기본값인 'media' 전략에서는 운영체제 설정만 따르므로 앱 내부에서 토글 버튼을 눌러도 아무 변화가 없습니다.

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

빌드 크기가 큰 경우

프로덕션 빌드에서 CSS 번들이 예상보다 크다면, content 경로 범위가 지나치게 넓거나 테스트 파일 같은 불필요한 대상까지 스캔하고 있을 가능성이 높습니다.

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

실무에서 자주 겪는 함정

  • 동적 클래스 문자열 조합: `text-${color}-500` 처럼 변수로 클래스를 조합하면 JIT 스캐너가 이를 인식하지 못해 프로덕션 빌드에서 스타일이 통째로 빠지는 경우가 흔합니다. 색상 종류가 적다면 조건문이나 매핑 객체로 완전한 클래스 이름을 명시하는 방식이 안전합니다.
  • tailwind-merge 활용: 조건부로 className을 이어 붙이다 보면 서로 충돌하는 유틸리티가 동시에 적용되는 문제가 생길 수 있습니다. tailwind-merge 라이브러리를 사용하면 나중에 전달된 클래스가 앞선 클래스를 올바르게 덮어쓰도록 정리해줍니다.
  • @apply의 적정 사용 범위: 모든 스타일을 @apply로 추출하면 결국 기존 CSS 파일 구조와 다를 바 없어지고, 반대로 유틸리티만 계속 쌓으면 컴포넌트 경계가 흐려집니다. 반복적으로 등장하는 버튼, 입력 필드 같은 패턴에만 제한적으로 @apply를 적용하는 것이 균형점입니다.
  • content 경로 누락: 스토리북이나 공용 UI 패키지처럼 별도 디렉터리에 있는 파일이 content 배열에서 빠지면 로컬 개발 환경에서는 문제없이 보이다가 프로덕션 빌드에서만 스타일이 사라지는 상황이 발생합니다. CI 파이프라인에서 빌드 후 CSS 파일 용량을 확인하는 단계를 추가해두면 이런 누락을 조기에 발견할 수 있습니다.

주의사항

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

Tailwind는 HTML에 클래스를 적은 순서가 아니라, 생성된 CSS 규칙의 순서로 우선순위가 결정됩니다. bg-red-500 bg-blue-500처럼 같은 속성을 지정하는 클래스를 함께 쓰면 어느 쪽이 적용될지 예측하기 어려우므로, 조건부로 클래스를 바꿔야 하는 상황이라면 tailwind-merge나 variant 기반 유틸리티(cva 등)를 사용하는 것이 안전합니다.

2. @apply 남용을 피하세요

@apply를 지나치게 많이 사용하면 유틸리티 우선 방식의 장점(마크업만 보고 스타일을 바로 파악할 수 있다는 점)이 사라집니다. 반복되는 패턴은 컴포넌트로 추출하고, 정말 공통화가 필요한 최소한의 경우에만 @apply를 적용하는 것이 좋습니다.

3. 프로덕션 빌드 체인을 통일하세요

개발 환경과 프로덕션 환경에서 스캔 경로나 최적화 설정이 다르면 “로컬에서는 정상인데 배포 후에만 스타일이 깨지는” 문제가 발생하기 쉽습니다. 빌드 스크립트와 content 설정을 팀 차원에서 한 번 고정해두고, 이후 변경 시에는 리뷰를 거치는 것을 권장합니다.

정리하면, Tailwind CSS는 초기 학습 곡선과 마크업이 길어 보이는 단점이 있지만, 팀에서 디자인 토큰과 컴포넌트 패턴에 대한 합의가 이루어지면 수정 속도와 유지보수 비용 측면에서 이점이 큰 프레임워크입니다. 위에서 다룬 설치 방법, 유틸리티 예제, 트러블슈팅 항목을 참고해 프로젝트 상황에 맞게 적용해보시기 바랍니다.

자주 묻는 질문 (FAQ)

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

A. 새로운 화면을 자주 구현해야 하거나, 팀의 디자인 시스템을 코드베이스에 직접 반영해야 하는 프로젝트에 적합합니다. 위에서 다룬 설치 과정과 유틸리티 예제를 그대로 적용해보면서, 팀에서 사용할 색상·간격 토큰과 tailwind-merge, content 경로 같은 규칙을 함께 정리해두면 도입이 수월해집니다.

Q. 이 글을 읽기 전에 알아두면 좋은 내용은?

A. 기본적인 CSS의 Flexbox, Grid, 미디어 쿼리 개념을 알고 있으면 유틸리티 클래스 이름을 빠르게 이해할 수 있습니다. 이 블로그의 CSS 및 PostCSS 관련 글을 함께 참고하면 전체적인 흐름을 잡는 데 도움이 됩니다.

Q. 더 깊이 공부하려면 어디를 참고해야 하나요?

A. Tailwind 공식 문서가 가장 정확하고 최신 정보를 담고 있습니다. v3에서 v4로 전환 중인 프로젝트라면 사용 중인 빌드 도구(Vite, Next.js, Astro 등)의 공식 통합 가이드도 함께 확인하는 것을 권장합니다.


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

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


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

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