Tailwind CSS 설정 및 사용
Tailwind의 유틸리티 우선 방식을 이해하고 설정·전역 지시어·반응형 클래스로 일관된 컴포넌트 UI를 만듭니다.
이전 절에서 CSS 모듈, Sass, 그리고 CSS-in-JS와 같은 다양한 스타일링 접근 방식을 살펴보았습니다.
각 방식은 고유한 장단점을 가지고 있으며, 프로젝트의 요구사항에 따라 선택됩니다.
이번 절에서는 프론트엔드에서 널리 쓰이는 Tailwind CSS의 개념과 사용 방식을 다룹니다.
Tailwind CSS는 유틸리티 우선(Utility-first) CSS 프레임워크로, 미리 정의된 작은 유틸리티 클래스를 HTML 마크업에 직접 적용해 스타일을 구성하는 방식입니다.
이 접근은 기존 CSS 작성 방식과 다른 패러다임을 제시하며, 빠른 개발 속도와 일관된 디자인 시스템 구축에 강점을 보입니다.
Next.js는 Tailwind CSS와의 통합을 매우 쉽고 효율적으로 지원합니다.
Tailwind CSS란 무엇인가요?
Tailwind CSS는 저수준(low-level) CSS 프레임워크입니다.
btn, card와 같은 의미론적인(semantic) 클래스 이름을 미리 정의하여 제공하는 대신, flex, pt-4, text-center, rotate-90 등과 같이 CSS 속성-값 쌍에 직접 매핑되는 수많은 유틸리티 클래스를 제공합니다.
- 유틸리티 우선(Utility-first): HTML 요소에
class속성으로 여러 개의 유틸리티 클래스를 조합하여 스타일을 만듭니다. - 컴파일 시점에 CSS 생성: 소스에서 실제로 사용한 유틸리티를 감지해 필요한 CSS를 생성하므로 런타임 스타일 엔진이 필요하지 않습니다.
- 높은 커스터마이징 가능성: Tailwind CSS 4에서는 전역 CSS의
@theme블록으로 색상, 폰트, 간격 같은 디자인 토큰을 확장할 수 있습니다. - 반응형 디자인 내장:
sm:,md:,lg:등과 같은 접두사를 사용하여 반응형 디자인을 쉽게 구현할 수 있습니다. - 의사 클래스(Pseudo-classes) 지원:
hover:,focus:,active:등과 같은 접두사를 사용하여 상호작용 상태의 스타일을 직접 적용할 수 있습니다.
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
버튼
</button>위 코드는 background-color, padding, font-weight, border-radius 등 여러 CSS 속성들을 유틸리티 클래스로 조합하여 하나의 버튼을 스타일링합니다.
왜 Tailwind CSS를 사용해야 할까요? (이점)
Tailwind CSS의 주요 이점은 다음과 같습니다.
- 빠른 개발 속도: CSS 파일을 오가며 클래스 이름을 고민하거나 새로 작성할 필요 없이, HTML 마크업 내에서 빠르게 스타일을 적용할 수 있습니다.
- 일관된 디자인 시스템: 공통 토큰의 간격·색상·타이포그래피를 재사용하기 쉽습니다. 임의 값도 사용할 수 있으므로 일관성은 팀의 사용 규칙에 달려 있습니다.
- 유지보수성 향상: 컴포넌트 마크업에서 적용한 유틸리티를 확인하기 쉽습니다. 유틸리티는 전역 CSS이므로 CSS Modules처럼 클래스 이름을 지역화하지는 않습니다.
- 필요한 유틸리티 생성: 탐지된 클래스에 해당하는 CSS를 생성합니다. 최종 크기는 사용한 클래스·변형과 추가 CSS의 양에 따라 달라집니다.
- 스타일 생성 런타임 불필요: 브라우저에서 JavaScript로 유틸리티 CSS를 생성하는 엔진이 필요하지 않습니다. CSS 계산·레이아웃·페인팅 비용까지 사라지는 것은 아닙니다.
- 모바일 우선 반응형 디자인: 접두사가 없는 클래스는 모든 화면 크기에 적용되고,
md:같은 접두사는 해당 breakpoint 이상에 적용됩니다.
Next.js에서 Tailwind CSS 설정 및 사용하기
Next.js는 Tailwind CSS를 위한 공식적인 설치 가이드를 제공하며, 설정 과정이 매우 간단합니다.
4판 설정을 준비하기
아래 PostCSS 플러그인·@import·@theme는 Tailwind CSS 4판 설정입니다. 3판 프로젝트의 지시어와 설정 파일을 그대로 섞지 않습니다. 이미 Tailwind가 연결된 프로젝트는 현재 설정과 의존성 판을 확인한 뒤 예제 페이지 세 파일을 추가하고, 아직 연결되지 않은 프로젝트만 설치 단계부터 진행합니다.
PostCSS 설정은 CSS 처리에 Tailwind를 연결하고, globals.css의 import는 스타일 입력을 제공하며, Root Layout의 import는 앱에 그 전역 CSS를 연결합니다. 이 중 한 입력이 빠지면 JSX에 클래스 이름만 적어도 원하는 규칙이 나타나지 않을 수 있습니다. 공식 PostCSS 설치 안내를 함께 비교하세요.
Next.js 프로젝트 생성 (또는 기존 프로젝트 사용):
새 프로젝트에서 create-next-app의 기본 권장 설정을 선택하면 Tailwind가 포함됩니다. 설정을 직접 고르는 경우 Tailwind 질문에 Yes를 선택합니다. 이미 설정된 프로젝트라면 설치와 설정 파일을 중복해서 추가하지 않습니다.
npx create-next-app@latest my-app-with-tailwind
# ? Would you like to use Tailwind CSS? Yes기존 프로젝트에 추가하는 경우 다음 단계를 따릅니다.
필요한 패키지 설치:
tailwindcss, 공식 Tailwind PostCSS 플러그인, PostCSS를 개발 의존성으로 설치합니다.
npm install -D tailwindcss @tailwindcss/postcss postcssPostCSS 플러그인 연결:
프로젝트 루트에 postcss.config.mjs를 만들고 Tailwind 플러그인을 등록합니다.
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};Tailwind CSS 4의 기본 설정에는 tailwind.config.js나 content 배열이 필요하지 않습니다.
전역 CSS에서 Tailwind 불러오기: Next.js의 전역 CSS 파일에서 Tailwind를 한 줄로 불러옵니다.
@import "tailwindcss";
@theme {
--color-brand-500: #2563eb;
}소스의 완전한 클래스 이름은 평문 토큰으로 탐지합니다. node_modules, .gitignore에 포함된 파일 등 기본 제외 범위에 필요한 소스가 있다면 @source로 명시합니다.
전역 CSS 파일을 Root Layout에 임포트:
src/app/layout.tsx 파일에 위에서 설정한 전역 CSS 파일을 임포트합니다.
import './globals.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ko">
<body>{children}</body>
</html>
);
}설정 후 스타일이 누락되면 증상에 맞는 입력부터 확인합니다.
NEXT · 9.4 · Tailwind CSS
스타일 누락은 처리 연결·소스 탐지·적용 조건을 확인합니다
Tailwind CSS 4 예제의 PostCSS 플러그인, globals.css의 @import, Root Layout의 import를 확인합니다. 특정 클래스만 빠진 경우에는 이름 탐지와 적용 조건을 구분합니다.
| 확인 대상 | 관련 입력 | 판단 기준 |
|---|---|---|
| 전체 유틸리티 | PostCSS 플러그인, globals.css, Root Layout import | CSS 처리 연결과 최종 스타일시트 로드를 각각 확인 |
| 동적 색상 클래스 | 조건별 완전한 클래스 이름 | bg-${tone}-600 같은 문자열 조각은 계산하지 않음; 완성된 이름으로 매핑 |
| 외부 패키지 클래스 | 자동 탐지 제외 경로와 @source | node_modules 등 탐지 밖의 필요한 소스를 명시 |
| 반응형 규칙 | 기본 클래스와 md: 등 접두사 | 기본은 모든 크기, 접두사는 해당 breakpoint 이상; 미디어 조건과 최종 규칙 확인 |
- 전체 유틸리티
- 관련 입력: PostCSS 플러그인, globals.css, Root Layout import판단 기준: CSS 처리 연결과 최종 스타일시트 로드를 각각 확인
- 동적 색상 클래스
- 관련 입력: 조건별 완전한 클래스 이름판단 기준: bg-${tone}-600 같은 문자열 조각은 계산하지 않음; 완성된 이름으로 매핑
- 외부 패키지 클래스
- 관련 입력: 자동 탐지 제외 경로와 @source판단 기준: node_modules 등 탐지 밖의 필요한 소스를 명시
- 반응형 규칙
- 관련 입력: 기본 클래스와 md: 등 접두사판단 기준: 기본은 모든 크기, 접두사는 해당 breakpoint 이상; 미디어 조건과 최종 규칙 확인
탐지기는 소스를 평문으로 읽고 알려진 유틸리티를 생성합니다. 완전한 클래스 문자열을 작성해도 화면의 적용 결과는 CSS 순서·미디어 조건·상태 조건까지 확인해야 합니다.
예시
tone이 blue라고 해도 bg-와 tone과 -600을 이어 만든 문자열을 소스 탐지기가 JavaScript처럼 실행하지 않습니다. 조건별로 bg-blue-600과 bg-red-600처럼 완전한 이름을 소스에 쓰고 그중 하나를 선택하면, 탐지 단계에 두 클래스가 보이고 화면에서는 선택한 클래스가 적용됩니다. 클래스가 CSS에 생성되는 조건과 해당 규칙이 현재 요소·미디어·상태에 적용되는 조건은 따로 확인합니다. 이는 클래스 탐지 공식 안내의 조건입니다.
소스 제외 경로에만 있는 클래스를 @source로 등록하는 일은 CSS 규칙 생성의 입력을 넓힙니다. 브라우저 클릭 핸들러를 실행하게 만들거나 서버에 데이터를 저장하는 설정은 아닙니다.
문자열 조합을 완전한 선택지로 바꾸기 다음은 두 색상만 허용하는 작은 컴포넌트입니다. 소스에 각 클래스 이름이 완전히 적혀 있고 실행할 때 그중 하나를 고릅니다. CSS 처리와 소스 탐지 범위가 연결되어 있다는 전제에서 사용합니다.
type Tone = 'blue' | 'red';
const toneClasses: Record<Tone, string> = {
blue: 'bg-blue-600 hover:bg-blue-700',
red: 'bg-red-600 hover:bg-red-700',
};
export function ToneButton({ tone }: { tone: Tone }) {
return <button className={`${toneClasses[tone]} text-white px-4 py-2`}>선택 색상</button>;
}이 버튼은 표시만 하고 클릭 상태를 바꾸는 핸들러는 없습니다. Tone은 개발 중의 타입 조건이며 외부 요청값을 실행 중에 검증하지는 않습니다. 외부 값으로 선택한다면 먼저 blue·red 중 하나인지 검사합니다. 폭처럼 연속적인 값을 표현할 때는 무한한 w-[...] 클래스 문자열을 조합하기보다 검증한 값을 인라인 스타일이나 CSS 변수로 전달하는 방식도 검토합니다.
생성된 규칙과 적용된 규칙 구분하기
기본 md:는 48 rem 이상이라는 최소 폭 조건입니다. md:flex-row는 작은 화면 전용이라는 뜻이 아니며, 경계 폭에서도 적용됩니다. 같은 요소에 같은 속성을 서로 다르게 지정하는 클래스를 함께 두면 HTML 클래스 나열 순서를 뒤집는 것으로 우선순위가 바뀌지 않습니다. 예를 들어 같은 계층·명시도에서 .flex 규칙 뒤에 .grid 규칙이 있다면 className="grid flex"여도 grid가 이깁니다. 실제 스타일시트 순서·CSS 계층·명시도를 확인하고, 의도한 값 하나를 조건으로 선택하는 편이 명확합니다.
Tailwind CSS 사용 예시
이제 컴포넌트나 페이지의 JSX/TSX 파일에서 Tailwind CSS 유틸리티 클래스를 직접 사용할 수 있습니다.
실습: Tailwind CSS로 버튼과 카드 컴포넌트 스타일링// 클라이언트 컴포넌트를 사용하기 위해 use client 지시어를 가진 파일 임포트
import TailwindButton from './TailwindButton';
import TailwindCard from './TailwindCard';
export default function TailwindCssPage() {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-100 p-8">
<h1 className="text-4xl font-bold text-gray-800 mb-8">
Tailwind CSS 예제
</h1>
<p className="text-lg text-gray-600 mb-12 text-center">
유틸리티 클래스를 사용하여 컴포넌트를 빠르게 스타일링합니다.
</p>
<div className="flex flex-col md:flex-row gap-8 w-full max-w-4xl">
{/* TailwindButton은 클라이언트 컴포넌트 */}
<TailwindButton />
{/* TailwindCard는 클라이언트 컴포넌트 */}
<TailwindCard title="Tailwind 카드" description="이것은 Tailwind CSS로 스타일링된 카드 컴포넌트입니다.">
<p className="text-gray-700">추가적인 내용입니다.</p>
<button className="mt-4 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors">
자세히 보기
</button>
</TailwindCard>
</div>
</div>
);
}"use client";
import React, { useState } from 'react';
export default function TailwindButton() {
const [count, setCount] = useState(0);
return (
<div className="flex flex-col items-center p-6 bg-white rounded-xl shadow-lg w-full md:w-1/2">
<h2 className="text-2xl font-semibold text-gray-700 mb-4">카운터 버튼</h2>
<p className="text-xl text-blue-600 mb-4">현재 카운트: <span className="font-bold">{count}</span></p>
<button
onClick={() => setCount(prev => prev + 1)}
className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-3 px-6 rounded-lg shadow-md transition-all duration-300 ease-in-out transform hover:scale-105 active:scale-95"
>
클릭하여 카운트 증가
</button>
<p className="mt-4 text-sm text-gray-500">이 버튼은 클라이언트 컴포넌트에서 상태를 관리합니다.</p>
</div>
);
}TailwindCard는 아래 원문에서 클라이언트 컴포넌트로 선언되어 있지만, 정적인 props와 children을 표시하는 기능만으로 클라이언트 경계가 필수인 것은 아닙니다. 상태를 사용하는 TailwindButton과 구분합니다.
"use client";
import React from 'react';
interface TailwindCardProps {
title: string;
description: string;
children?: React.ReactNode;
}
export default function TailwindCard({ title, description, children }: TailwindCardProps) {
return (
<div className="bg-white p-8 rounded-xl shadow-lg hover:shadow-xl transition-shadow duration-300 w-full md:w-1/2">
<h2 className="text-3xl font-bold text-gray-800 mb-4">
{title}
</h2>
<p className="text-gray-600 mb-6 leading-relaxed">
{description}
</p>
{children}
</div>
);
}위에서 설명한 Tailwind CSS 설치 및 설정 (postcss.config.mjs, globals.css)을 완료합니다.
src/app/tailwind-css 폴더를 만들고 위 page.tsx, TailwindButton.tsx, TailwindCard.tsx 파일을 생성합니다.
개발 서버(npm run dev)를 실행한 후, http://localhost:3000/tailwind-css로 접속합니다.
- Tailwind CSS 클래스들이 적용된 깔끔한 UI를 확인할 수 있습니다.
- 클릭하여 카운트 증가 버튼을 눌러 상태 변화와 스타일이 잘 작동하는지 확인합니다.
- 배경색이 바뀌는 hover 상태와 카운트가 바뀌는 클릭을 따로 확인합니다. hover·active는 CSS 적용 조건이고, 카운트 증가는 React의 클릭 핸들러와 상태 갱신입니다. 스타일이 보여도 핸들러가 실행됐다는 증거는 아닙니다.
- 브라우저 개발자 도구를 열어 요소들의 클래스 목록을 확인하면, CSS-in-JS나 CSS 모듈처럼 해시값이 붙지 않고 작성된 유틸리티 클래스 이름 그대로 나타나는 것을 볼 수 있습니다.
Tailwind CSS의 장점과 고려사항
화면 폭·클래스 선택·상태를 독립적으로 예상하기
기본 breakpoint를 바꾸지 않았고 1rem=16px이며 다른 CSS 규칙이 없다고 가정합니다. 원 페이지의 두 컴포넌트를 담은 flex flex-col md:flex-row 구역은 viewport 600 px와 800 px에서 각각 어느 방향인가요? 이어 카운터 버튼을 두 번 클릭하면 0에서 어떤 값이 되는지 설명하세요. 마지막으로 bg-문자열 조합만 있던 코드를 완전한 두 클래스 이름의 매핑으로 바꾸면 왜 다시 빌드할 때 탐지 결과를 기대할 수 있는지 적습니다.
조건이 적용되는 세 시점을 구별한 해설
기본 md는 48 rem이므로 이 전제에서 768 px 이상입니다. 600 px에서는 기본 flex-col, 800 px에서는 md:flex-row가 적용됩니다. 공식 반응형 안내는 접두사 없는 규칙과 breakpoint 이상의 규칙을 구분합니다. breakpoint 설정을 바꾸면 이 문제의 경계값도 함께 바꾸어야 합니다.
카운터는 React가 이전 값을 받아 1을 더하는 함수형 갱신을 두 번 처리하므로 2가 됩니다. Tailwind의 hover·active 클래스가 카운트 값을 바꾸는 것은 아닙니다. 완전한 클래스 이름의 매핑은 소스 탐지기의 입력에 그 이름을 제공하지만, 소스가 실제 탐지 대상이고 CSS 처리가 연결돼 있다는 조건도 필요합니다. 탐지 입력 수정과 브라우저의 이미 로드된 스타일시트가 갱신되는 과정까지 구별합니다.
- 극강의 개발 생산성: CSS 파일을 오가지 않고 HTML/JSX 내에서 거의 모든 스타일을 구현할 수 있습니다.
- 규칙 재사용: 공통 토큰과 유틸리티 조합을 컴포넌트에 모아 여러 화면에서 재사용할 수 있습니다.
- 긴 클래스 목록: 복잡한 컴포넌트의 경우
class속성이 매우 길어질 수 있습니다.@apply지시어를 사용하여 반복되는 유틸리티 클래스 묶음을 커스텀 클래스로 추상화할 수 있지만, 이는 Tailwind의 핵심 철학에서 벗어날 수 있습니다. - 초기 학습 곡선: 기존 CSS에 익숙한 개발자는 유틸리티 우선 방식에 적응하는 데 시간이 걸릴 수 있습니다. 모든 스타일이 클래스 이름으로 어떻게 매핑되는지 학습해야 합니다.
- 동적 값의 표현: 상태에 따라 이미 완성된 클래스 이름을 선택할 수 있습니다. 연속적인 값은 CSS 변수나 인라인 스타일로 전달할 수 있으며, 문자열 조각을 이어 붙인 새로운 클래스 이름은 소스 탐지기가 계산하지 않습니다.
- 직접 CSS 작성의 감소: 때로는 직접 CSS를 작성해야 할 필요성을 느끼지 못하게 되어, CSS의 기본 개념에 대한 이해도가 낮아질 수 있다는 비판도 있습니다.
추가 연습: 탐지·미디어·우선순위·클릭 구분하기
- 소스에는
bg-${tone}-600조합만 있고, 완전한 색상 클래스는 다른 탐지 대상에도 없습니다. 런타임에 tone이 blue가 되면 탐지기가 그 코드 실행 결과를 알아서 계산할까요? 위 매핑으로 바꾸면 어떤 입력이 달라질까요? - 기본 md=48rem, 1rem=16px이며 다른 규칙이 없다고 가정합니다.
flex flex-col md:flex-row는 viewport 767 px와 768 px에서 각각 어떤 방향일까요? - 같은 CSS 계층·명시도에서
.flex { display: flex }뒤에.grid { display: grid }가 있고 다른 규칙은 없습니다.class="grid flex"와class="flex grid"중 어느 쪽이 grid일까요? 혼동을 줄이는 코드는 어떻게 만들까요? - 원 TailwindButton의 카운트는 0입니다. 버튼 위에 포인터만 올린 뒤 클릭을 두 번 했습니다. hover만 한 시점과 두 클릭 뒤의 카운트는 무엇일까요? 위 ToneButton도 자동으로 같은 카운트를 증가시킬까요?
각 조건이 처리되는 단계의 풀이
- 계산하지 않습니다. 탐지기는 소스를 평문으로 읽으며 프로그램의 문자열 조합을 실행하지 않습니다. 매핑에는
bg-blue-600,bg-red-600과 hover 클래스의 완전한 이름이 생깁니다. 그 소스가 실제 탐지 대상이고 CSS 처리가 연결돼 있어야 생성 입력에 반영됩니다. 런타임에는 허용한 색상에 맞는 이미 생성된 클래스를 선택합니다. - 767 px에서는 column, 768 px에서는 row입니다. 48×16=768이므로 md 규칙은 768 px 이상에서 적용됩니다. 접두사 없는 기본 column은 md보다 작은 폭에서 유지됩니다. breakpoint를 바꾸거나 추가 CSS가 있으면 그 조건도 다시 적용해야 합니다.
- 두 경우 모두 grid입니다. 제시된 조건에서는 스타일시트에서 뒤에 있는
.grid가 같은 속성을 덮습니다. HTML의 클래스 문자열 순서는 이 규칙 순서를 바꾸지 않습니다. grid와 flex 중 원하는 클래스 하나만 조건에 따라 선택하면 같은 속성의 충돌을 피할 수 있습니다. - hover만 했을 때는 0, 두 클릭 뒤에는 2입니다. 증가를 실행하는 것은
setCount(prev => prev + 1)이 연결된 클릭 핸들러입니다. ToneButton에는 그 핸들러와 카운트 상태가 없으므로 색상·hover 클래스를 붙였다는 이유로 카운트가 증가하지 않습니다.