노트

Tailwind CSS

Tailwind CSS

프런트엔드#css#tooling · 연결된 개념 13개

쉽게 말하면

Tailwind는 '여백 4', '글자 작게' 같은 작은 스타일 블록을 마크업에 붙여 조합하는 CSS 도구예요. 빌드할 때 실제로 쓴 블록의 CSS만 만들어서, 스타일 파일을 오가며 이름 짓는 수고가 줄어요.

비유가 깨지는 곳 블록을 고르는 일은 빌드 때 끝나요. Tailwind는 JS를 실행하지 않고 소스 문자열만 찾으니, 조각을 이어 붙여 만든 클래스는 CSS가 안 생겨요. 클래스는 완전한 문자열로 써야 해요.

flex, px-4, text-sm처럼 속성 하나를 담은 유틸리티 클래스(Utility Class)를 마크업에 조합해 스타일을 입히는 CSS 프레임워크. 빌드할 때 소스를 훑어 실제로 쓰인 클래스의 CSS만 만들어 내는 빌드 도구이고, 런타임에 돌아가는 JS(JavaScript)는 없다.

실행 시점을 나눠 보기

  • 빌드 타임: Tailwind가 소스 파일을 스캔해 클래스 이름처럼 생긴 문자열을 모으고, 그에 맞는 정적 CSS 파일을 만든다
  • 서버 런타임: 서버 컴포넌트(React Server Components)든 SSR(Server-Side Rendering)이든 서버는 class="flex gap-4"가 붙은 HTML을 만들 뿐이다
  • 브라우저: CSS를 내려받아 스타일 계산·레이아웃·페인트를 한다 → 브라우저 렌더링 파이프라인

그래서 Tailwind에는 "서버 컴포넌트용 CSS"가 따로 없고, Pages Router에서 App Router로 옮길 때 달라지는 것도 전역 CSS를 불러오는 위치(_app → 루트 layout) 정도다. 클라이언트 컴포넌트에서 상태에 따라 클래스를 바꾸는 것도 이미 만들어진 CSS 규칙 중 다른 것이 매칭될 뿐이다.

완전한 클래스 문자열만 잡힌다

Tailwind는 JS를 실행하지 않고 문자열을 찾기만 한다. 조각을 이어 붙인 클래스는 소스에 존재하지 않으므로 CSS가 만들어지지 않는다.

// 위험: bg-blue-500이라는 문자열이 소스에 없다
<div className={`bg-${color}-500`} />
 
// OK: 두 문자열이 모두 소스에 있다
<div className={active ? 'bg-blue-500' : 'bg-gray-500'} />
const colors = { blue: 'bg-blue-500', gray: 'bg-gray-500' }

알아 둘 기능

  • Preflight: 기본으로 들어가는 리셋. 제목·목록의 브라우저 기본 스타일이 사라진다
  • @apply: 반복되는 유틸리티 묶음을 클래스 하나로 모은다. 버튼·입력처럼 작은 요소에만 쓰고, 카드처럼 큰 덩어리를 추상화하면 마크업만 보고 스타일을 알 수 있는 장점이 사라진다. 직접 만든 클래스는 @layer components에 넣어야 유틸리티로 덮어쓸 수 있다 → CSS @layer
  • group·peer: 부모의 상태(group-hover:)나 앞쪽 형제의 상태(peer-invalid:)에 따라 스타일을 바꾼다. group/이름으로 중첩 그룹을 구분한다 → 속성 선택자와 형제 선택자
  • @theme(v4): 디자인 토큰(Design Token)을 등록하면 CSS 변수와 유틸리티 클래스가 함께 생긴다. v3까지는 tailwind.config.js의 theme.extend에 적었다
  • space-y-4는 자식들 사이에만 간격을 준다(첫 자식 위는 제외). 지금은 gap이 더 많이 쓰인다

기존 CSS 프레임워크에서 점진 전환하기

Bootstrap 같은 프레임워크를 한 번에 걷어내기는 어렵다. 화면들이 클래스뿐 아니라 프레임워크의 리셋과 기본값(폼 줄 높이, 버튼 커서, 테두리 색, SVG 정렬)에 암묵적으로 기대고 있기 때문이다. 그래서 둘이 한동안 공존하도록 설계하고, 변환은 규칙과 스크립트에 맡긴다(Tailwind v4 기준).

  • 우선순위 충돌: v4는 유틸리티를 네이티브 캐스케이드 레이어 @layer utilities 안에 넣는다. 레이어 밖에 있는 기존 CSS는 명시도와 상관없이 레이어 안의 일반 선언을 이긴다. 그래서 공존기에는 important 플래그로 유틸리티를 !important로 만들거나(!important끼리는 레이어 순서가 뒤집혀 레이어 쪽이 이긴다), 기존 CSS를 @import ... layer(legacy)처럼 낮은 레이어에 넣는다
  • 이름 충돌: flex·text-*·bg-*처럼 이름이 겹치면 prefix(tw)로 Tailwind 클래스를 tw:flex처럼 분리한다. 리뷰에서 어느 쪽 스타일인지 바로 보인다. 전환이 끝나면 프리픽스를 일괄 제거한다
  • 리셋 충돌: Preflight는 base 레이어에 들어가 마진·테두리·제목·폼 기본값을 바꾼다. 전환 안 된 화면까지 흔들리므로 처음에는 Preflight를 빼고 theme.css·utilities.css만 불러온 뒤, 전환이 끝난 영역에만 리셋을 거는 식으로 범위를 좁힌다
@layer theme, base, components, utilities;
@import 'tailwindcss/theme.css' layer(theme) prefix(tw);
@import 'tailwindcss/utilities.css' layer(utilities) important;  /* prefix는 @theme이 든 theme.css 쪽에 건다 */
  • 변환은 사람이 아니라 규칙이: d-flex→flex, justify-content-between→justify-between 같은 일대일 대응은 린트 경고와 치환 스크립트(codemod)로 수백 파일을 한 번에 바꾼다. 이때 "유틸리티는 바꾸되 폼 상태처럼 동작에 필요한 컴포넌트 클래스는 보호한다"는 예외 목록이 핵심이다
  • 호환 계층은 임시로: 패키지를 지운 뒤에도 남은 클래스 이름을 받쳐 주는 작은 호환 CSS를 두고, 남은 클래스를 다 치운 뒤 지운다. 마지막엔 포매터(prettier-plugin-tailwindcss)로 클래스 순서를 맞춘다
  • 기본값 차이는 따로 잡는다: 대량 변환 뒤에 드러나는 회귀가 진짜 일이다. 예를 들어 v4 Preflight에서 SVG·이미지는 display: block이 되어 인라인 아이콘 배치가 틀어지고, 테두리 색은 v3의 회색이 아니라 currentColor라 구분선이 진하게 나온다. 폼 요소의 줄 높이, 비활성 버튼 스타일, button의 cursor: pointer도 직접 넣어야 한다. 화면 비교와 e2e 셀렉터 수정까지 전환 범위로 잡는다

결국 목표는 클래스 이름 교체가 아니라, 스타일의 소유권을 전역 캐스케이드에서 컴포넌트 마크업으로 옮기는 것이다. 바꾸는 방식 자체는 병렬 수정 (팽창-수축)·차세대와 고도화와 같은 고민이다.

왜 많이 쓰나, 대가는

  • 런타임 비용이 없어 RSC·스트리밍과 잘 맞는다 → CSS-in-JS와 빌드 타임 CSS
  • 유틸리티를 공유하니 CSS 총량이 프로젝트 크기와 함께 늘지 않고, 마크업을 지우면 CSS도 빠진다
  • 이름 짓기와 스타일 파일 왕복이 사라지고, 스케일(m-3, m-4)에서 고르게 되어 간격·색이 통일된다
  • 대신 마크업이 길어진다. 변형은 cva로 묶고, 클래스 충돌은 tailwind-merge와 cn()로 정리한다. 사용자가 고른 임의 색처럼 런타임 값은 인라인 스타일이나 CSS 변수로 넘긴다

출처: Tailwind CSS — Detecting classes in source files: Dynamic class names · Tailwind CSS — Using the prefix option · Tailwind CSS — Preflight · Tailwind CSS — Upgrade guide: Default border color · MDN — @layer · Adam Wathan — CSS Utility Classes and "Separation of Concerns"

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • Svelte

    컴포넌트를 빌드할 때 DOM(Document Object Model)을 직접 바꾸는 JS(JavaScript)로 컴파일하는 UI 프레임워크. React처럼 런타임에 가상 DOM(Virtual DOM)을 비교하지 않고, 어떤 값이 바뀌면 어떤 DOM을 고칠지 컴파일러가 미리 코드로 만들어 둔다. 앱 프레임워크는 SvelteKit이다.

  • CSS 명시도

    캐스케이드에서 레이어까지 비겼을 때, 선택자가 요소를 얼마나 구체적으로 가리키는지로 승부를 내는 점수. (A, B, C) 세 자리로 매기고 앞자리부터 비교한다.

  • CSS 캐스케이드

    같은 요소의 같은 속성에 여러 규칙이 값을 줄 때, 브라우저가 어느 값을 쓸지 정하는 규칙. 흔히 명시도(Specificity)만 떠올리지만 그보다 앞서 비교하는 기준들이 있다.

  • 템플릿 리터럴 타입

    템플릿 리터럴 타입(Template Literal Types)은 문자열 리터럴 타입을 템플릿 문자열 문법으로 조합해 새 문자열 유니언을 만드는 기능이다. 유니언을 넣으면 가능한 모든 조합이 나온다.

보기 옵션