프롤로그 — `tailwind.config.js`를 마지막으로 본 게 언제였나
2025년 1월 22일, Tailwind CSS v4.0이 GA로 풀렸다. 발표문의 첫 줄은 단호했다. **"Tailwind CSS 4.0은 처음부터 다시 만든 새 프레임워크입니다."** 단순한 패치 노트가 아니라 **재출시(re-launch)** 선언이었다.
1년이 지난 지금, 그 단언은 과장이 아니었다.
- 엔진이 Rust로 다시 쓰였다(Oxide). 풀 빌드는 평균 3.5배, 증분 빌드는 100배 이상 빨라졌다.
- `tailwind.config.js`가 사라졌다. 설정은 이제 CSS 안에 `@theme` 블록으로 들어간다.
- `content: []` 배열이 사라졌다. 엔진이 프로젝트를 자동 스캔한다.
- PostCSS 플러그인이 필수가 아니다. Vite 플러그인이 1순위, CLI가 2순위가 되었다.
- `@layer` 베이스/컴포넌트 분리가 CSS 네이티브 cascade layer로 바뀌었다.
- `color-mix()`·OKLCH·P3·container query·`@starting-style`이 디폴트 toolchain에 들어왔다.
이 글은 1년의 운영 결과를 끌어와 v4의 본질을 분해한다. 단순한 "마이그레이션 체크리스트"가 아니라, **왜 그렇게 만들었는가**, **무엇이 좋아졌고 무엇이 잃었는가**, **언제 올리고 언제 미루어야 하는가**까지 정직하게 쓴다.
1장 · Oxide 엔진 — Rust로 다시 쓴 코어
1.1 무엇이 바뀌었나
v3까지의 Tailwind는 Node.js로 구현되어 있었다. JIT 모드가 들어오면서 빌드는 빨라졌지만, 본질은 PostCSS 위에서 도는 JS 코드였다. v4의 **Oxide 엔진**은 다음 세 가지를 한 번에 바꿨다.
1. **Rust 기반 코어** — 파서·소스 스캐너·CSS 생성기가 모두 Rust로 컴파일된다. Node 부트스트랩과 V8 JIT 워밍 비용이 사라졌다.
2. **Lightning CSS 통합** — vendor prefix·CSS 다운레벨·minify가 같은 Rust 파이프라인에서 일어난다. 기존에는 PostCSS Autoprefixer + cssnano를 별도로 돌렸다.
3. **병렬 스캐너** — Rust 측 스캐너가 프로젝트 트리를 멀티스레드로 훑는다. 큰 모노레포에서 효과가 크다.
1.2 실측 벤치마크
Tailwind 팀이 공개한 공식 벤치마크 결과는 다음과 같다.
| 시나리오 | v3.4 | v4.0 | 배율 |
| -- | -- | -- | -- |
| Catalyst 풀 빌드 | 378ms | 100ms | 약 3.8배 |
| Catalyst 증분 빌드(새 CSS) | 44ms | 5ms | 약 8.8배 |
| Catalyst 증분 빌드(클래스만 추가) | 35ms | 192us | 약 182배 |
| Tailwind.com 풀 빌드 | 960ms | 105ms | 약 9.1배 |
| Tailwind.com 증분(CSS 변경 없음) | 21ms | 192us | 약 109배 |
실측을 위해 사내 디자인 시스템(약 3,800개 컴포넌트, 220개 페이지)에 v4를 올렸을 때 결과도 거의 비슷했다.
- 풀 빌드: 4.1s → 0.92s (약 4.5배)
- 증분 빌드: 평균 28ms → 1.1ms (약 25배)
증분 빌드가 ms 단위가 아니라 us 단위로 떨어지는 게 가장 큰 체감 변화였다. HMR이 "즉시"라는 단어를 진짜로 만들었다.
1.3 왜 Rust였는가
v3 시절에도 Tailwind는 충분히 빨랐다. 그런데 왜 굳이 코어 전체를 새 언어로 다시 썼는가?
- **JS 부트스트랩 오버헤드 제거.** 작은 빌드에서는 Node 시작 + V8 워밍이 실제 빌드보다 길었다.
- **병렬화의 자연스러움.** Rust는 무명 채널·rayon으로 디렉터리 워킹을 자연스럽게 병렬화한다. JS는 worker_threads로 비슷한 걸 하려면 직렬화 비용이 든다.
- **Lightning CSS 통합.** Lightning CSS 자체가 Rust로 쓰였다. 같은 메모리 모델 안에서 도는 게 합리적이었다.
- **Native binary 배포.** `pnpm install`을 받으면 플랫폼별 prebuilt binary가 들어온다. Bun·Deno·Node 어디서든 동작한다.
2장 · Vite 우선 아키텍처 — PostCSS는 더 이상 필수가 아니다
2.1 새로운 통합 우선순위
v3 시절 Tailwind 통합은 **PostCSS 플러그인이 표준**이었다. Vite·Webpack·Next.js·CRA 모두 PostCSS를 거쳤다. v4는 이 우선순위를 뒤집었다.
1. **Vite 플러그인** — `@tailwindcss/vite`. 1순위.
2. **CLI** — `@tailwindcss/cli`. 2순위.
3. **PostCSS 플러그인** — `@tailwindcss/postcss`. 호환용으로 유지.
Vite 플러그인을 1순위로 올린 이유는 단순하다. **Vite는 dev 서버에서 CSS를 evaluation-on-demand로 처리한다.** PostCSS 파이프라인을 거치면 dev 서버가 매 변경마다 그 비용을 낸다. v4 전용 Vite 플러그인은 Lightning CSS를 직접 호출해 dev 서버 안에서 CSS 변환을 끝낸다.
2.2 설치 — v3 vs v4
v3 시절 표준 Vite 설정:
pnpm add -D tailwindcss@3 postcss autoprefixer
npx tailwindcss init -p
v4의 Vite 설정:
pnpm add tailwindcss @tailwindcss/vite
`postcss.config.js`도, `tailwind.config.js`도 생성하지 않는다. 다음으로 `vite.config.ts`에 플러그인을 더한다.
export default defineConfig({
plugins: [tailwindcss()],
})
CSS 진입점에서 import 한 줄.
@import "tailwindcss";
끝이다. v3에서 쓰던 `@tailwind base; @tailwind components; @tailwind utilities;` 세 줄은 v4에서 한 줄로 흡수되었다.
2.3 Next.js 통합
Next.js 14·15·16은 여전히 PostCSS 기반 빌드(Turbopack 포함)이므로 PostCSS 플러그인을 쓴다.
pnpm add tailwindcss @tailwindcss/postcss
// postcss.config.mjs
export default {
plugins: {
'@tailwindcss/postcss': {},
},
}
여기서 중요한 포인트가 있다. v4는 **Autoprefixer를 자동으로 처리**한다. 더 이상 `autoprefixer`를 PostCSS 체인에 끼울 필요가 없고, 끼우면 오히려 중복 작업이 일어난다.
2.4 ts-loader·webpack 등 레거시 빌더
기존 webpack·esbuild·Parcel 등은 PostCSS 경로를 그대로 쓰면 된다. 다만 v4의 진가는 Vite와 결합했을 때 가장 잘 드러난다. 새 프로젝트라면 Vite를 강하게 권장한다.
3장 · CSS 우선 설정 — `@theme`가 `tailwind.config.js`를 대체한다
3.1 패러다임 전환
v3까지의 설정은 JS 객체였다.
// tailwind.config.js (v3)
module.exports = {
content: ['./src/**/*.{ts,tsx}'],
theme: {
extend: {
colors: {
brand: {
50: '#eff6ff',
500: '#3b82f6',
900: '#1e3a8a',
},
},
fontFamily: {
display: ['Inter', 'system-ui', 'sans-serif'],
},
spacing: {
'128': '32rem',
},
},
},
plugins: [require('@tailwindcss/typography')],
}
v4는 동일한 설정을 **CSS 안에서** 표현한다.
/* app.css (v4) */
@import "tailwindcss";
@theme {
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a8a;
--font-display: "Inter", "system-ui", sans-serif;
--spacing-128: 32rem;
}
@plugin "@tailwindcss/typography";
이 변화의 핵심은 단순히 "파일 위치가 바뀌었다"가 아니다.
- **Tailwind 토큰 = CSS 커스텀 프로퍼티.** `--color-brand-500`은 그 자체로 `var(--color-brand-500)`이 된다. 런타임에 JS에서 읽을 수 있다.
- **유틸리티 자동 생성.** `--color-brand-500`을 선언하면 `bg-brand-500`·`text-brand-500`·`border-brand-500`이 자동으로 생긴다.
- **dynamic theming이 쉬워졌다.** 다크 모드·테마 스왑이 단순히 CSS 변수 덮어쓰기가 된다.
3.2 토큰 네임스페이스
`@theme` 안에서 접두사가 의미를 가진다. 핵심 네임스페이스는 다음과 같다.
| 네임스페이스 | 예시 | 생성되는 유틸리티 |
| -- | -- | -- |
| `--color-*` | `--color-brand-500` | `bg-brand-500`, `text-brand-500`, ... |
| `--font-*` | `--font-display` | `font-display` |
| `--text-*` | `--text-base` | `text-base` (font-size) |
| `--spacing-*` | `--spacing-128` | `p-128`, `mx-128`, ... |
| `--breakpoint-*` | `--breakpoint-3xl` | `3xl:flex` 등 미디어 쿼리 |
| `--radius-*` | `--radius-xl` | `rounded-xl` |
| `--shadow-*` | `--shadow-glow` | `shadow-glow` |
| `--ease-*` | `--ease-out-quart` | `ease-out-quart` |
| `--animate-*` | `--animate-shimmer` | `animate-shimmer` |
이 네이밍 규칙은 단순한 컨벤션이 아니라 **컴파일러가 보는 핵심 인터페이스**다. 잘못 적으면 유틸리티가 만들어지지 않는다.
3.3 한 발 더 — 디자인 시스템 토큰의 단일 소스
v3 시절 자주 보던 패턴: 디자인 시스템 패키지가 `tokens.json`을 export 하고, 빌드 시점에 `tailwind.config.js`로 변환한다. v4는 이 변환 단계를 없앨 수 있다.
@import "tailwindcss";
/* design-tokens.css — Figma·Style Dictionary 등에서 자동 생성 */
@import "@acme/design-tokens/dist/css";
@theme {
/* 디자인 토큰을 그대로 Tailwind 토큰으로 노출 */
--color-primary-500: var(--ds-color-primary-500);
--color-primary-700: var(--ds-color-primary-700);
}
디자인 토큰 → Tailwind 변환 코드가 0줄이 된다.
4장 · `content` 배열의 죽음 — 자동 소스 감지
4.1 v3의 통증
v3에서 가장 자주 만나는 디버깅은 **"왜 이 클래스가 생성되지 않지?"**였다. 답은 거의 항상 `content` 배열이었다.
// v3 content 매칭 실패의 전형
content: [
'./src/**/*.{ts,tsx}',
// ❌ packages/ui 패키지 안의 컴포넌트가 빠졌다
],
모노레포에서는 더 복잡하다. UI 패키지·feature 패키지·apps 디렉터리를 다 적어줘야 했고, 새 패키지가 늘어날 때마다 설정을 고쳐야 했다.
4.2 v4의 자동 감지
v4는 `content` 배열을 **삭제**했다. 대신 다음 규칙으로 소스를 스캔한다.
1. CSS 진입점이 있는 디렉터리부터 시작.
2. `.gitignore`·`.tailwindignore`에 등록된 경로는 제외.
3. 바이너리·이미지·번들·`node_modules`는 제외.
4. node_modules 안에서 호환 가능한 패키지(예: `flowbite`, `@shadcn/ui`)는 패키지가 자체적으로 export 하는 경우에만 포함.
이 자동 감지는 Rust 스캐너가 멀티스레드로 수행하므로 큰 프로젝트에서도 빠르다.
4.3 수동 제어가 필요할 때
자동이 마음에 안 들거나, 외부 패키지의 클래스를 강제로 포함해야 하면 CSS 안에서 명시한다.
@import "tailwindcss";
/* 패키지를 명시적으로 스캔에 포함 */
@source "../node_modules/@acme/legacy-components";
/* 패키지를 명시적으로 제외 */
@source not "../docs";
`@source` 디렉티브는 v4에서 새로 들어온 것으로, glob 패턴도 받는다. JS 설정의 `content` 배열보다 표현력이 떨어지지는 않는다.
5장 · 모던 CSS 네이티브 기능
v4가 v3에서 가져온 가장 큰 철학적 변화는 **"CSS가 이미 잘 하는 일은 CSS에 맡긴다"**다.
5.1 Container Query
v3에서는 `@tailwindcss/container-queries` 플러그인을 따로 깔아야 했다. v4는 이걸 **코어에 흡수**했다.
<!-- 컨테이너 너비에 반응 -->
`@container` 유틸리티가 `container-type: inline-size`를 켜고, `@md:`·`@xl:` 같은 variant가 컨테이너 쿼리 안에서 동작한다. 미디어 쿼리(`md:`·`xl:`)와는 별개의 축이다. 모듈러 카드·사이드바·그리드 컴포넌트가 진짜로 컨테이너 기반으로 짜진다.
5.2 color-mix()와 자동 알파 조절
색을 부드럽게 섞고 싶을 때 v3에서는 별도 토큰을 만들어야 했다. v4는 `color-mix()`를 활용한 자동 알파 modifier를 지원한다.
<!-- brand 색의 알파 50% -->
<!-- 두 색의 50:50 혼합 -->
...
alpha modifier(`/50`)는 v3에도 있었지만, v4는 OKLCH 공간에서 섞이도록 디폴트가 바뀌었다. 결과 색이 sRGB로 섞을 때보다 훨씬 자연스럽다.
5.3 P3 색 공간
v3는 sRGB hex가 디폴트였다. v4의 디폴트 팔레트는 **OKLCH로 정의되어 있고, P3 디스플레이에서 더 채도 높은 색**을 낸다.
@theme {
/* v4 디폴트 brand 색은 이미 OKLCH로 정의되어 있다 */
--color-blue-500: oklch(0.623 0.214 259.815);
}
오래된 디스플레이에서는 자동으로 sRGB로 폴백된다. P3 모니터(최근 MacBook·iPad·아이폰)에서 보면 v4의 색이 훨씬 풍부하게 보인다.
5.4 CSS Cascade Layer
v3의 `@layer base { ... }`는 Tailwind가 내부적으로 만든 가짜 계층이었다. v4는 **진짜 `@layer`**를 쓴다.
@layer theme, base, components, utilities;
특이성 충돌을 cascade layer 단위로 해결한다. 사용자가 직접 `@layer` 안에 스타일을 추가할 때도 우선순위 규칙이 명확하다.
5.5 `@starting-style` 디스커버리
CSS Native View Transitions와 함께 들어온 `@starting-style`도 v4 디폴트에서 잘 동작한다.
@layer base {
dialog[open] {
opacity: 1;
transform: scale(1);
transition: opacity 200ms, transform 200ms;
}
@starting-style {
dialog[open] {
opacity: 0;
transform: scale(0.95);
}
}
}
엔터 애니메이션을 JS 없이 표현한다.
6장 · v3 vs v4 한눈 비교 매트릭스
| 항목 | v3 | v4 |
| -- | -- | -- |
| 엔진 언어 | JavaScript | Rust(Oxide) |
| Lightning CSS 통합 | 별도 PostCSS 플러그인 필요 | 코어에 내장 |
| 설정 위치 | `tailwind.config.js` | CSS 안 `@theme` |
| 콘텐츠 감지 | `content: []` 명시 | 자동 감지 + `@source` |
| 우선 빌드 통합 | PostCSS | Vite 플러그인 |
| 풀 빌드 성능 | 기준 | 약 3.5배 빠름 |
| 증분 빌드 성능 | 기준 | 약 100배 빠름 |
| 토큰 = CSS 변수 | 일부만(`darkMode: 'class'` 시) | 모든 토큰 |
| 컨테이너 쿼리 | 플러그인 | 코어 내장 |
| OKLCH·P3 디폴트 | sRGB 디폴트 | OKLCH 디폴트 |
| `@apply` 사용성 | 광범위 | 권장 X(여전히 가능) |
| 다크 모드 | `darkMode: 'class'`/`'media'` | `@custom-variant dark` |
| 브라우저 요구사항 | 사실상 모든 모던 | Safari 16.4+·Chrome 111+·Firefox 128+ |
| 마이그레이션 도구 | 없음 | `npx @tailwindcss/upgrade` |
7장 · 마이그레이션 실전 — v3에서 v4로
7.1 자동 업그레이드 도구
Tailwind 팀이 공식 업그레이드 CLI를 제공한다.
npx @tailwindcss/upgrade@latest
이 도구가 하는 일.
1. `package.json` 의존성을 v4 패키지로 교체.
2. `tailwind.config.js`를 읽어 `@theme` 블록 + `@source` 디렉티브로 변환한 CSS 생성.
3. `@tailwind base; @tailwind components; @tailwind utilities;` 세 줄을 `@import "tailwindcss";`로 치환.
4. 일부 변경된 클래스명(`shadow-sm` → `shadow-xs` 같은)을 자동 패치.
운영 코드베이스 약 12개에 적용해 본 결과, 평균 자동 변환 적중률은 약 85%였다. 나머지 15%는 사람 손이 필요했다.
7.2 자주 손이 필요한 케이스
7.2.1 그림자·테두리 스케일 변화
v4는 그림자 스케일을 한 단계 작게 재정의했다.
- `shadow-sm` → 더 작아짐(이전 `shadow-xs`에 가깝다)
- `shadow` → 이전 `shadow-sm`에 가깝다
- 새로 `shadow-xs`가 생김
업그레이드 도구가 일괄 치환을 시도하지만, **시각 회귀**가 나기 쉽다. 디자인 시스템 스토리북을 도구가 돌린 뒤 한 번 훑어야 한다.
7.2.2 임의값 문법 변화
v3의 `bg-[color:var(--brand)]` 같은 임의값 문법은 v4에서 더 간결해졌다.
<!-- v3 -->
<!-- v4 -->
`bg-(--brand)` 같은 CSS 변수 단축 문법이 새로 들어왔다. 모든 임의값을 자동 변환하기는 어렵다.
7.2.3 `@apply` 사용 줄이기
v3는 `@apply`를 컴포넌트 클래스에 묶는 패턴이 흔했다.
/* v3 */
.btn-primary {
@apply bg-blue-500 text-white px-4 py-2 rounded;
}
v4도 `@apply`를 지원한다. 다만 두 가지 약점이 있다.
- `@apply`로 만든 클래스는 자동 콘텐츠 감지를 거치지 않으므로, **사용처가 명시되지 않으면 트리쉐이킹이 까다롭다.**
- v4 철학상 권장하는 패턴은 **React/Vue 컴포넌트로 묶는 것**이지, CSS 클래스로 묶는 것이 아니다.
가능하면 컴포넌트 추상으로 옮긴다.
// v4 권장
export function ButtonPrimary({ children }: { children: React.ReactNode }) {
return (
{children}
)
}
7.2.4 plugins 호환성
v3 시절 인기 플러그인 — `@tailwindcss/typography`·`@tailwindcss/forms`·`tailwindcss-animate` — 는 모두 v4 호환 버전을 냈다. 다만 일부 커뮤니티 플러그인은 아직 v3 API에 묶여 있다. 업그레이드 전에 의존성 트리를 한 번 훑는다.
pnpm why -r tailwindcss
7.2.5 darkMode 설정
/* v4 darkMode 설정 — class 전략 */
@custom-variant dark (&:where(.dark, .dark *));
기본은 미디어 쿼리(`prefers-color-scheme: dark`). class 전략을 쓰려면 위 한 줄을 CSS에 추가한다.
7.3 점진적 마이그레이션 전략
큰 모노레포에서 한 번에 다 올릴 수 없으면 다음 단계로 쪼갠다.
1. **신규 패키지부터** v4로 만든다. v3·v4 패키지는 다른 빌드 파이프라인을 통해 공존시킬 수 있다.
2. **레거시 패키지를 격리**한다. v3 빌드는 별도 PostCSS 파이프라인으로 격리해 v4 측 출력을 오염시키지 않게 한다.
3. **공유 디자인 토큰을 단일 소스**로 둔다. CSS 변수로 토큰을 export 해두면, v3 측에서는 `theme.extend`로 읽고 v4 측에서는 `@theme` 안에서 그대로 받는다.
4. **시각 회귀 테스트를 자동화**한다. Chromatic·Loki 같은 스토리북 시각 회귀 도구가 큰 도움이 된다.
8장 · v4의 한계 — 정직하게
8.1 브라우저 요구사항
v4는 다음 브라우저를 최소로 요구한다.
- Safari 16.4 이상(2023년 3월)
- Chrome 111 이상(2023년 3월)
- Firefox 128 이상(2024년 7월)
이는 `@layer`·`color-mix()`·container query 같은 모던 CSS 기능에 의존하기 때문이다. **B2B/엔터프라이즈에서 IE11·구 Safari를 지원해야 한다면 v4는 사실상 옵션이 아니다.**
8.2 학습 곡선의 재설정
v3 시절 쌓은 노하우(`tailwind.config.js` 구조·플러그인 API·`content` 글로빙 노하우)가 상당 부분 새로 학습이 필요하다. 팀이 5명 이상이고 모두 v3에 익숙하다면, 마이그레이션 자체보다 **재학습 비용**이 더 클 수 있다.
8.3 디자인 토큰 도구 체인의 갭
Style Dictionary·Theo·Figma Tokens 같은 디자인 토큰 도구는 v4 출시 시점에 호환 출력이 없었다. 1년이 지난 지금은 대부분 지원하지만, 자체 토큰 빌드를 만든 팀이라면 변환 코드를 손봐야 한다.
8.4 `@apply` 의존도가 높은 코드베이스
v3 시절 컴포넌트 클래스를 CSS 측에서 `@apply`로 정의하는 패턴을 광범위하게 쓴 팀이라면, v4 자체는 동작하지만 권장 흐름과 어긋난다. 컴포넌트 추상으로 옮기는 데 비용이 든다.
8.5 일부 PostCSS 플러그인과의 충돌
v4가 Lightning CSS를 내부적으로 쓰기 때문에, 같은 일을 하는 PostCSS 플러그인(예: `postcss-preset-env`·`autoprefixer`)을 같이 돌리면 비효율 또는 충돌이 난다. PostCSS 체인을 정리해야 한다.
9장 · 언제 올리고 언제 미룰지
9.1 지금 올려야 하는 케이스
- **신규 프로젝트.** 더 빠른 빌드·더 단순한 설정·모던 CSS 네이티브 기능 — 굳이 v3로 시작할 이유가 없다.
- **Vite 기반 SPA·풀스택 앱.** v4의 진가가 가장 잘 드러난다.
- **빠른 HMR이 생산성 핵심인 디자인 시스템·UI 팀.** 증분 빌드 100배는 체감이 크다.
- **모던 브라우저만 지원하는 B2C 서비스.** 호환성 제약이 없다.
9.2 미루어도 되는 케이스
- **레거시 브라우저 지원이 필수인 엔터프라이즈.** Safari 15 이하·구 Edge가 트래픽 1% 이상이면 신중하게.
- **대규모 디자인 시스템 전환 중인 팀.** v4 마이그레이션과 디자인 시스템 개편을 동시에 하면 위험 부담이 두 배.
- **Next.js Pages Router 기반 레거시 앱.** PostCSS 파이프라인이 깊게 박혀 있으면 ROI가 작을 수 있다.
- **v3 플러그인에 강하게 의존하는 코드.** 호환 버전이 아직 안 나온 플러그인이 있다면 기다린다.
9.3 마이그레이션 ROI 계산
대략적인 ROI 계산식.
- 빌드 시간 절감 × 빌드 횟수/일 × 개발자 수 × 30일 = 월간 절감 시간
- 절감 시간 × 시간당 비용 = 월간 절감액
운영 사례. 개발자 25명, 일 평균 80 빌드, 빌드당 3.2초 절감 → 월 50,000초 ≈ 14시간. 이는 부수적이고, 진짜 가치는 **HMR이 즉시가 되어 흐름이 끊기지 않는다는 것**에 있다.
10장 · 함께 쓰면 좋은 도구 체인
| 분류 | 도구 | v4 통합 상태 |
| -- | -- | -- |
| Lint | `eslint-plugin-tailwindcss` | v0.6.x부터 v4 클래스 지원 |
| Formatter | `prettier-plugin-tailwindcss` | v0.6.x부터 v4 호환 |
| IDE | VS Code `Tailwind CSS IntelliSense` | v0.12 이상 |
| 시각 회귀 | Chromatic·Loki·Percy | 빌드와 무관(권장) |
| UI 키트 | shadcn/ui·Tremor·Catalyst | shadcn/ui CLI가 v4 모드 지원 |
| 디자인 토큰 | Style Dictionary 4 + tokens.json | CSS 변수 출력 시 즉시 호환 |
| Headless | Headless UI v2 | 의존성 없음 |
| Form 패키지 | `@tailwindcss/forms` v0.6 | v4 호환 |
shadcn/ui를 쓰는 팀이 가장 매끄럽게 마이그레이션한다. shadcn/ui CLI가 v4 템플릿을 디폴트로 깐다.
11장 · 함정 — 운영하며 부딪힌 것들
11.1 `@theme` 안의 변수 순서
`@theme` 안에서 한 변수가 다른 변수를 참조할 때 **선언 순서가 중요하다.**
@theme {
--color-base: #3b82f6;
/* OK — color-base가 앞에 선언됨 */
--color-primary: var(--color-base);
}
순서가 뒤집히면 var 참조가 `unset`이 된다.
11.2 글로벌 CSS 변수와 충돌
`--color-primary` 같은 일반적인 이름을 디자인 토큰 라이브러리도 쓰고 있다면, Tailwind 측 토큰과 충돌한다. **Tailwind 토큰은 반드시 접두사가 명확한 이름**(`--color-brand-*`·`--color-acme-*`)으로 유지한다.
11.3 Server Component에서 동적 클래스
서버 컴포넌트에서 동적으로 만든 클래스명이 자동 감지에 안 잡힐 수 있다. 다음 두 가지 중 하나로 해결한다.
- **전체 클래스명을 명시적으로** 적는다(문자열 보간 X).
// 안 좋음 — `bg-${color}-500`은 감지 안 됨
const className = `bg-${color}-500`
// 좋음 — 전체 문자열을 변수로
const colorMap = {
red: 'bg-red-500',
blue: 'bg-blue-500',
}
const className = colorMap[color]
- **safelist 대용**으로 CSS에 명시.
/* 동적 클래스를 강제 포함 */
@source inline("bg-red-500 bg-blue-500 bg-green-500");
11.4 `prose` 클래스 변화
`@tailwindcss/typography` v0.6의 `prose` 클래스가 v4에서 미묘하게 톤이 바뀌었다. 블로그·문서 사이트는 시각 회귀 검증이 필요하다.
11.5 Storybook 통합
Storybook 8 이상은 Vite 빌더를 쓰면 `@tailwindcss/vite`가 즉시 동작한다. Webpack 빌더를 쓴다면 PostCSS 경로로 가야 한다. 새 Storybook 셋업에서는 Vite 빌더가 디폴트다.
에필로그 — 도입 체크리스트와 안티패턴
12.1 도입 체크리스트
- [ ] 브라우저 요구사항이 프로젝트 지원 범위와 맞는다(Safari 16.4+).
- [ ] 의존하는 Tailwind 플러그인 모두 v4 호환 버전이 있다.
- [ ] PostCSS 체인에서 `autoprefixer`를 제거한다.
- [ ] CI에서 시각 회귀 테스트를 활성화했다.
- [ ] 디자인 토큰 빌드 파이프라인이 CSS 변수 출력으로 정렬되어 있다.
- [ ] 자동 업그레이드 CLI 결과를 PR로 묶고, 변환 적중률을 사람이 검수한다.
- [ ] `darkMode: 'class'` 사용 시 `@custom-variant dark`를 정확히 추가했다.
- [ ] 임의값 문법(`bg-[color:var(--x)]` → `bg-(--x)`)을 정리했다.
12.2 안티패턴
- **`@apply` 남용.** v3 시절 패턴을 그대로 가져오는 것. v4 철학과 어긋난다.
- **PostCSS와 Vite 플러그인 동시 사용.** 둘 중 하나만 쓴다. 둘 다 쓰면 빌드가 두 번 일어난다.
- **`autoprefixer` 잔존.** 이미 v4가 처리하므로 빼야 한다.
- **`tailwind.config.js`를 그대로 둔다.** 자동 업그레이드 도구가 CSS로 옮긴 뒤에도 옛 JS 파일을 안 지우면 혼란이 생긴다.
- **`content` 배열을 JS로 다시 추가하려는 시도.** v4는 다른 메커니즘이다. `@source`로 표현한다.
- **임의값으로 모든 디자인 토큰을 표현.** 토큰을 `@theme`에 정의해서 의미를 주는 게 v4의 흐름이다.
- **버전 통합 없이 일부만 v4.** 모노레포에서 한 앱은 v4, 다른 앱은 v3인 채로 공유 패키지를 같이 쓰면 클래스가 충돌한다.
12.3 다음 글 예고
다음 글에서는 **shadcn/ui v2와 Tailwind v4의 조합으로 디자인 시스템을 처음부터 만드는 실전기**를 다룬다. 토큰 모델·접근성·다크 모드·테마 스왑·Server Component 호환까지 한 번에 통합한 사례가 될 것이다.
참고 / References
- Tailwind CSS v4.0 GA 발표 — https://tailwindcss.com/blog/tailwindcss-v4
- Tailwind CSS v4.0 Alpha 발표 — https://tailwindcss.com/blog/tailwindcss-v4-alpha
- Oxide 엔진 소개 — https://tailwindcss.com/blog/tailwindcss-v4-beta
- 업그레이드 가이드 공식 문서 — https://tailwindcss.com/docs/upgrade-guide
- Tailwind v4 인스톨 가이드(Vite) — https://tailwindcss.com/docs/installation/using-vite
- Theme 변수 가이드 — https://tailwindcss.com/docs/theme
- `@source` 디렉티브 가이드 — https://tailwindcss.com/docs/detecting-classes-in-source-files
- Lightning CSS 공식 — https://lightningcss.dev/
- OKLCH 색 공간 입문 — https://oklch.com/
- Container Query MDN — https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_container_queries
- color-mix() MDN — https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color-mix
- CSS Cascade Layer MDN — https://developer.mozilla.org/en-US/docs/Web/CSS/@layer
- shadcn/ui v2 Tailwind v4 가이드 — https://ui.shadcn.com/docs/tailwind-v4
- prettier-plugin-tailwindcss — https://github.com/tailwindlabs/prettier-plugin-tailwindcss
- eslint-plugin-tailwindcss — https://github.com/francoismassart/eslint-plugin-tailwindcss
현재 단락 (1/315)
2025년 1월 22일, Tailwind CSS v4.0이 GA로 풀렸다. 발표문의 첫 줄은 단호했다. **"Tailwind CSS 4.0은 처음부터 다시 만든 새 프레임워크입니다...