class="flex items-center gap-4 bg-blue-500"처럼, 이미 정해진 아주 작은 CSS 조각 하나하나(유틸리티 클래스)를 HTML에 직접 나열해서 화면을 만드는 CSS 프레임워크다. Bootstrap처럼 "카드", "버튼" 같은 완성된 컴포넌트를 주는 게 아니라, margin-top·display:flex 하나짜리 레고 블록만 잔뜩 준다.
@theme으로 옮겼고, 클래스를 찾아 CSS를 만들어내는 핵심 엔진을 자바스크립트에서 러스트(Rust)로 새로 짰다. 그 결과 대규모 프로젝트에서도 빌드가 훨씬 빨라졌고, 별도 설정 없이도 소스 파일을 자동으로 찾아 스캔한다.
tailwindlabs/tailwindcss · 라이선스 MIT(저작권자 Tailwind Labs, Inc.) · 핵심 패키지 버전 4.3.3 · pnpm 워크스페이스 + Turborepo + Cargo 워크스페이스 혼합 모노레포 · TypeScript 약 11만 4천 줄 + Rust 약 2만 1천 줄 · 최신 커밋 6de87c6(2026-07-28) · 정리 기준일 2026-07-29)
보통 웹사이트를 꾸밀 때는 style.css 같은 별도 파일에 .card { padding: 16px; border-radius: 8px; }처럼 이름 붙인 스타일 묶음을 만들고, HTML에서는 그 이름(클래스)만 갖다 쓴다. Tailwind CSS는 이 흐름을 뒤집는다. padding: 16px 하나에는 p-4, border-radius: 8px 하나에는 rounded-lg처럼 "CSS 속성 하나 = 미리 정의된 클래스 하나"를 대응시켜 두고, 개발자는 HTML에 이 작은 클래스들을 나란히 붙여 쓰기만 한다.
그래서 최종 결과물은 <div class="flex items-center gap-4 p-6 rounded-lg bg-white shadow"> 같은 모습이 된다. 새 CSS 파일을 만들지도, 클래스 이름을 고민하지도 않는다. 대신 Tailwind는 프로젝트의 HTML·JSX·템플릿 파일들을 직접 스캔해서 실제로 쓰인 클래스만 골라 최종 CSS를 만들어낸다 — 이 스캐너가 이번 딥다이브의 핵심이다.
보통 CSS는 팔레트에서 색을 직접 섞는 것과 같다 — 자유롭지만 매번 새로 배합해야 하고, 사람마다 결과물이 미묘하게 다르다. Tailwind는 "이미 배합이 끝난 물감통에 번호(클래스 이름)만 붙여 놓은 세트"를 준다. bg-blue-500이라는 번호만 고르면 항상 같은 파랑이 나온다.
그리고 v4부터는 이 물감통 목록 자체를 별도 설명서(자바스크립트 설정 파일)가 아니라 물감통이 놓인 진열대(CSS 파일) 위에 붙은 라벨(@theme)로 직접 적어 둔다. 설명서를 따로 펼쳐 보지 않아도, 진열대만 보면 어떤 색이 있는지 바로 알 수 있는 셈이다.
v3까지는 색상표·폰트·간격 같은 디자인 값들을 tailwind.config.js라는 자바스크립트 파일에서 관리했다. v4는 이걸 CSS 파일 안의 @theme 블록으로 옮겼다. 실제로 저장소의 packages/tailwindcss/theme.css를 열어보면 이렇게 시작한다.
@theme default {
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', ...;
--color-red-500: oklch(63.7% 0.237 25.331);
--color-red-600: oklch(57.7% 0.245 27.325);
...
}
자바스크립트 객체가 아니라 그냥 CSS 커스텀 프로퍼티(변수)다. 색상값도 예전의 #3b82f6 같은 16진수 대신 oklch()라는, 사람 눈에 더 고르게 밝기가 분포하는 색공간을 쓴다. "설정이 곧 CSS"가 되면서 빌드 도구가 자바스크립트를 평가(evaluate)할 필요가 없어졌고, 이것이 속도 개선의 큰 축 하나다.
프로젝트 안의 어떤 파일에 어떤 클래스가 쓰였는지 찾아내는 작업(스캐닝)은 파일 수가 많아질수록 무거워진다. v4는 이 스캐너와 클래스 추출기를 러스트로 새로 짰다 — 저장소의 crates/oxide 디렉터리(크레이트 이름 tailwindcss-oxide)가 그 실체다. Node.js에서는 @tailwindcss/oxide라는 이름의 네이티브 애드온(napi 바인딩, crates/node)으로 불러 쓴다. "느린 자바스크립트 정규식 스캐너"에서 "컴파일된 네이티브 코드"로 바뀐 셈이라, 특히 파일이 몇만 개인 대형 모노레포에서 체감 속도 차이가 크다.
v0·Claude·Cursor 같은 AI 코딩 도구가 만들어주는 화면 대부분이 Tailwind 클래스를 쓴다. 이유는 단순하다 — "별도 CSS 파일을 새로 만들거나 이름을 고민할 필요 없이, 클래스 이름 나열만으로 스타일이 끝난다"는 성질이 LLM이 한 번에 완결된 코드를 뱉기에 유리하기 때문이다. shadcn/ui 같은 인기 컴포넌트 모음도 Tailwind 위에서 동작한다. "사람이 배우기 쉬운가"와는 별개로, "기계가 생성하기 쉬운가"라는 축에서 지금 가장 널리 쓰이는 CSS 방식이 됐다.
UnoCSS·Windi CSS 같은 후발 주자들이 "더 빠르다"는 각도로 도전했지만, Tailwind는 v4에서 스스로 러스트 엔진을 들여와 그 속도 격차를 메웠다. 방대한 생태계(공식 문서·플러그인·shadcn/ui 같은 컴포넌트 모음·에디터 자동완성)가 여전히 가장 큰 해자(垓子)다. 자세한 대안 비교는 10번 섹션에서 다룬다.
전통적인 CSS 작성 방식에서 개발자가 실제로 겪는 세 가지 마찰을 Tailwind는 정면으로 겨냥한다.
버튼 하나의 여백을 바꾸려면 HTML을 열어 클래스 이름을 확인하고, CSS 파일로 옮겨가 그 이름을 찾고, 값을 고치고, 다시 브라우저로 돌아와 확인한다. Tailwind는 class="p-4"를 class="p-6"으로 바꾸는 순간 그 자리에서 결과가 보이게 만들어, 파일을 오가는 왕복(context switch) 자체를 없앤다.
.card-header-highlighted-v2 같은 이름을 지어본 사람이라면 안다 — 스타일 자체보다 "이 클래스를 뭐라고 부를지" 고민하는 데 더 많은 시간이 든다는 것을. Tailwind는 이름을 지을 필요 자체를 없앤다. flex·gap-4·text-center는 이미 정해진 이름이고, 그 이름이 곧 그 스타일의 설명이다.
큰 프로젝트일수록 "이 클래스, 아직도 쓰이나?"를 확신하기 어려워 아무도 못 지우는 CSS가 쌓인다. Tailwind는 애초에 실제 소스 파일에 등장한 클래스만 최종 CSS로 만든다(4번·5번 섹션에서 다룰 "후보 스캐너"). 안 쓰는 클래스는 결과물에 아예 존재하지 않으므로, "죽은 CSS가 쌓이는지"를 걱정할 필요가 원천적으로 줄어든다.
display:flex 하나·padding:1rem 하나처럼 CSS 속성 딱 하나(또는 아주 소수)에 대응하는 아주 작은 단위의 클래스. Tailwind의 flex, p-4, text-center가 전부 유틸리티 클래스다.
이 저장소는 언어가 다른 두 모노레포가 한 지붕 아래 겹쳐 있는 구조다. 자바스크립트/타입스크립트 쪽은 pnpm-workspace.yaml이 관리하고, 러스트 쪽은 Cargo.toml의 [workspace] members = ["crates/*"]가 관리한다. 두 세계를 잇는 다리가 crates/node다 — 러스트 코드를 자바스크립트에서 함수처럼 호출할 수 있게 감싸는 napi(Node API) 바인딩 크레이트다.
화살표로 요약하면 "러스트 스캐너(oxide)가 클래스 후보 문자열을 찾아냄 → 자바스크립트 코어(tailwindcss)가 그 문자열을 실제 CSS 규칙으로 컴파일 → PostCSS/Vite/Webpack/CLI 같은 '어댑터'가 각자의 빌드 파이프라인에 연결"이라는 한 방향 파이프라인이다. 이 어댑터들이 여러 개 있는 이유는 프로젝트마다 쓰는 빌드 도구가 다르기 때문이지, 핵심 컴파일 로직이 여러 벌 존재하는 게 아니다 — 전부 tailwindcss 코어 패키지 하나를 불러 쓴다.
import해서 부를 수 있게 이어주는 다리. crates/node가 이 역할을 해서, 자바스크립트 코드는 러스트 스캐너의 존재를 거의 의식하지 않고 함수처럼 호출한다.
이 섹션이 이 문서에서 가장 중요하다. class="hover:bg-blue-500 md:p-8" 같은 텍스트 한 줄이 실제로 브라우저가 읽는 CSS로 바뀌기까지, Tailwind 내부에서는 세 단계가 순서대로 일어난다. 하나씩 뜯어본다.
가장 신기한 지점은 여기다. Tailwind의 스캐너는 지금 보고 있는 파일이 HTML인지, JSX인지, Vue 템플릿인지, Ruby(ERB)인지 전혀 모른다. 그 언어의 문법을 파싱하지 않고, 그냥 "클래스 이름처럼 생긴 글자 뭉치"를 문자 단위로 훑으며 찾아낸다. 실제로 저장소의 crates/oxide/src/extractor/ 안에는 candidate_machine.rs·utility_machine.rs·variant_machine.rs·arbitrary_value_machine.rs·bracket_stack.rs 같은 파일들이 있는데, 이름 그대로 각자 "기계(machine)" — 한 글자씩 읽으며 상태를 옮겨가는 유한 상태 기계(state machine)다.
이 방식 덕분에 Tailwind는 React·Vue·Svelte·Ruby on Rails·PHP·Go 템플릿 등 어떤 언어의 파일이든 별도 파서 없이 스캔할 수 있다. 대신 "가끔 클래스가 아닌 문자열을 클래스로 착각하거나, 반대로 놓치는" 경계 사례가 생기는데, 이건 9번 섹션에서 다룬다.
이 스캐너는 외국어를 몰라도 신문 더미에서 "네모 상자 안의 숫자"만 찾아내는 사람과 비슷하다. 그 사람은 신문이 어느 나라 말로 쓰였는지, 무슨 내용인지 전혀 몰라도 "네모 상자"라는 생김새 패턴만 보고 정확히 숫자를 골라낸다. Tailwind 스캐너도 HTML 문법이나 JSX 문법을 이해하는 게 아니라, class="..."나 className={...} 근처의 "클래스처럼 생긴 글자 뭉치"라는 생김새 패턴만 보고 후보를 골라낸다.
v3까지는 tailwind.config.js의 content: ['./src/**/*.html', ...]처럼 "어느 폴더를 스캔할지" 사람이 직접 목록을 적어야 했다. 새 폴더를 만들고 이 목록에 추가하는 걸 깜빡하면 그 폴더의 클래스는 조용히 무시됐다. v4는 crates/oxide/src/scanner/auto_source_detection.rs를 통해 프로젝트 루트에서 시작해 .gitignore에 걸리지 않는 파일을 자동으로 다 훑는다. node_modules나 바이너리 파일(이미지·폰트 등)은 기본적으로 건너뛰도록 확장자 목록(BINARY_EXTENSIONS_GLOB)이 미리 정의돼 있다.
그래도 예외적으로 스캔 대상을 손보고 싶을 때는 CSS 안에서 @source "../shared-ui";처럼 경로를 명시적으로 추가하거나, @source not "./legacy";로 특정 폴더를 제외할 수 있다. "기본은 자동, 예외만 수동"으로 뒤집힌 것이 핵심 변화다.
@theme: 디자인 토큰이 CSS 커스텀 프로퍼티가 되는 순간
packages/tailwindcss/src/index.ts를 보면 CSS를 처음부터 훑으면서 @theme이라는 이름의 at-rule(@media·@import처럼 @으로 시작하는 CSS 규칙)을 만날 때마다 특별 취급하는 코드가 있다(if (node.name === '@theme')). 이때 Features.AtTheme라는 플래그를 켜고, @theme prefix(tw)처럼 접두어 옵션이 붙어 있으면 그 값의 유효성(소문자 알파벳인지)까지 검사한다. 이 블록 안에 적힌 --color-brand: oklch(...) 같은 변수들이, 나중에 bg-brand 같은 새 유틸리티 클래스가 자동으로 존재하게 만드는 원천이다.
즉 "이 값이 존재한다"는 선언(@theme)과 "이 값을 어떻게 쓸지"(bg-*, text-* 같은 유틸리티 문법)가 분리돼 있다. --color-brand라는 이름 규칙만 지키면, Tailwind가 자동으로 bg-brand·text-brand·border-brand 전부를 만들어준다.
많은 사람이 "v4는 JS 설정을 완전히 없앴다"고 오해하지만, 저장소의 packages/tailwindcss/src/compat/ 폴더를 열어보면 이야기가 다르다. apply-compat-hooks.ts는 CSS 안에 @config "./tailwind.config.js";라고 명시적으로 선언한 경우에 한해 옛 설정 파일을 읽어 들이는 로직을 담고 있다. resolve-config.ts·plugin-api.ts(옛 plugin() API)·legacy-utilities.ts까지, v3 생태계를 위한 상당한 분량의 호환 코드가 살아있다.
다시 말해 "기본값은 CSS-first, 필요하면 옵트인(opt-in)으로 옛 방식도 계속 쓸 수 있다"가 정확한 설명이다. 대규모 조직이 하루아침에 수백 개 설정을 옮길 수 없다는 현실을 반영한 설계다.
Tailwind 코어가 만든 CSS AST(추상 구문 트리)는 아직 "브라우저 벤더 접두사 처리"나 "중첩(nesting) 문법을 옛 브라우저용으로 풀어쓰기", "압축(minify)" 같은 마무리 작업 전 상태다. 이 마지막 단계는 packages/@tailwindcss-node/src/optimize.ts에서 Lightning CSS(lightningcss 패키지, Parcel 번들러 제작자가 만든 별도의 러스트 기반 CSS 처리기)를 불러 처리한다. 즉 Tailwind 자체 엔진(Oxide)과 Lightning CSS는 서로 다른 러스트 프로젝트이며, Tailwind가 후자를 도구로 가져다 쓰는 관계다.
둘 다 러스트로 짜여 있어서 같은 프로젝트처럼 보이기 쉽지만, crates/oxide(클래스 후보 스캐너)와 lightningcss(별도 npm 패키지, CSS 파싱·압축)는 다른 저장소, 다른 팀이 만든 다른 도구다. Tailwind는 전자를 자체 개발하고, 후자는 의존성으로 가져다 쓴다.
줄 수만 보면 러스트 비중이 작아 보이지만(전체 소스의 약 16%), 러스트가 맡은 부분은 파일 시스템을 훑고 문자열을 파싱하는 가장 뜨거운(hot path) 구간이라 체감 성능 기여는 줄 수 비율보다 훨씬 크다. 반대로 CSS 규칙을 조립하고 디자인 시스템을 관리하는 "두뇌"에 해당하는 로직은 여전히 타입스크립트가 맡는다.
| 계층 | 구성 요소 / 왜 그 선택인가 |
|---|---|
| 코어 엔진 | packages/tailwindcss — 후보 파싱(candidate.ts, 약 1,237줄)·AST(ast.ts, 약 1,582줄)·변형자(variants.ts, 약 1,317줄)·유틸리티 정의(utilities.ts, 약 6,827줄로 저장소 최대 단일 파일) |
| 스캐너 엔진 | crates/oxide(크레이트명 tailwindcss-oxide) — 파일 탐색(scanner/)과 문자 단위 추출기(extractor/, 11종의 "머신" 파일) |
| Node 브리지 | crates/node(크레이트명 tailwind-oxide, napi 3.x 사용) — 러스트 함수를 자바스크립트에서 호출 가능하게 컴파일. 13개 OS/아키텍처별 네이티브 바이너리로 배포되며, 지원 밖 환경을 위한 WASM 폴백(wasm32-wasi)도 별도 패키지로 준비돼 있다 |
| 파일 탐색 유틸 | crates/ignore — 코드 검색 도구 ripgrep과 같은 계열의 .gitignore 인식 디렉터리 워커. rayon으로 파일 스캔을 병렬화 |
| 최종 CSS 손질 | lightningcss(별도 러스트 프로젝트, npm 의존성) — 벤더 접두사·중첩 폴백·압축을 @tailwindcss/node가 호출 |
| 모노레포 오케스트레이션 | pnpm 워크스페이스(패키지 설치·의존성 링크) + Turborepo(turbo.json, 빌드 캐싱·태스크 그래프) + Cargo 워크스페이스(러스트 빌드) |
| 버전 카탈로그 | pnpm-workspace.yaml의 catalog: 섹션이 lightningcss·postcss·vite·webpack 같은 공용 의존성 버전을 한 곳에서 고정(모든 패키지가 같은 버전을 쓰게 강제) |
| 패치 의존성 | patches/ 폴더에 @parcel/watcher·lightningcss의 pnpm 패치가 존재 — 남이 만든 의존성이라도 필요하면 소스를 직접 고쳐서 쓴다는 뜻 |
| 테스트 | Vitest(자바스크립트, 118개 *.test.ts) + cargo test(러스트) + Playwright(브라우저 UI 테스트) + crates/oxide/fuzz(퍼징 — 무작위 입력으로 크래시를 찾는 테스트) |
import처럼 부를 수 있게 해주는 표준 인터페이스. crates/node가 이 규격으로 러스트 스캐너를 감싼다.Tailwind는 "설치 방법이 여러 갈래"라는 인상을 주기 쉬운데, 실제로는 "어떤 빌드 도구를 쓰는가"에 따라 어댑터 패키지만 다르고 코어는 동일하다.
| 환경 | 설치·요구사항 |
|---|---|
| Vite 프로젝트 | npm i -D tailwindcss @tailwindcss/vite → vite.config.ts에 플러그인 등록. PostCSS 단계 없이 Vite에 직결돼 가장 빠른 조합으로 안내됨 |
| PostCSS 기반(Next.js 등) | npm i -D tailwindcss @tailwindcss/postcss → postcss.config.mjs에 등록 |
| Webpack/Rspack | @tailwindcss/webpack 로더 등록 |
| CLI만 필요할 때 | npm i -D tailwindcss @tailwindcss/cli → npx tailwindcss -i input.css -o output.css |
| Node.js를 아예 안 쓸 때 | @tailwindcss/standalone — Bun으로 빌드된 단일 실행파일. Node.js 설치 자체가 필요 없다 |
| 빌드 도구 없이 프로토타입만 | @tailwindcss/browser — <script src="..."> 한 줄로 브라우저 안에서 즉석으로 클래스를 컴파일(실서비스용 아님, 실험용) |
| 실행 환경 | 코어는 최신 Node.js LTS면 충분하지만, 러스트 네이티브 바이너리가 지원하지 않는 희귀 플랫폼에서는 자동으로 WASM 버전으로 대체됨(속도는 느려짐) |
| 브라우저 호환 | 생성된 CSS 자체가 oklch()·컨테이너 쿼리 등 비교적 최신 CSS 기능을 기본값으로 쓰므로, 아주 오래된 브라우저(예: 구형 Safari)에서는 색상 표현 등이 다르게 보일 수 있음(공식 문서가 브라우저 최저 버전을 명시) |
v3까지는 npx tailwindcss init으로 설정 파일을 먼저 만들고 content 배열을 채워야 했다. v4는 CSS 파일에 @import "tailwindcss"; 한 줄만 있으면 바로 동작한다 — 4번 섹션에서 설명한 "자동 소스 감지" 덕분에 별도 설정 파일 생성 단계 자체가 선택 사항이 됐다.
Vite 프로젝트를 기준으로 실제 하루 작업 흐름을 그대로 따라가 본다.
프로젝트의 메인 CSS 파일(보통 app.css 하나) 맨 위에 @import "tailwindcss";만 적는다. 이 한 줄이 앞서 본 @layer theme, base, components, utilities;와 함께 기본 색상표·폰트·리셋 스타일(preflight.css)을 전부 끌어온다.
<button class="rounded-lg bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">처럼 클래스를 붙이면, 개발 서버가 파일 저장을 감지해 러스트 스캐너가 다시 훑고, 새로 등장한 클래스만 추가로 CSS에 반영한다. 브라우저는 거의 즉시 새로고침된다.
@apply나 컴포넌트로 묶는다같은 클래스 조합을 여러 버튼에 반복해서 쓴다면, CSS 쪽에서 .btn { @apply rounded-lg bg-blue-500 px-4 py-2; }처럼 이름을 하나 붙이거나, React/Vue라면 그 조합을 가진 <Button> 컴포넌트를 만들어 재사용한다. "유틸리티 클래스 자체를 없애는" 게 아니라 "묶어서 재사용"하는 방향이 권장된다.
@theme만 건드린다브랜드 색을 바꿔야 한다면 CSS 파일에 @theme { --color-brand: oklch(60% 0.2 250); }만 추가하면 bg-brand·text-brand가 프로젝트 전체에서 즉시 쓸 수 있게 된다. 자바스크립트 설정 파일을 열 필요가 없다.
vite build 같은 프로덕션 빌드를 돌리면, 실제로 소스에 등장한 클래스만 모아 Lightning CSS로 압축·벤더 접두사 처리를 거친 최종 CSS 파일 하나가 나온다. 안 쓴 유틸리티 클래스 수천 개는 애초에 결과물에 존재하지 않는다.
@import "tailwindcss"; /* 진입점: 이 한 줄이 전부 */
@theme {
--color-brand: oklch(60% 0.2 250); /* 새 디자인 토큰 선언 */
}
.btn {
@apply rounded-lg bg-brand px-4 py-2 text-white; /* 반복 조합 묶기 */
}
이 코드는 그대로 복사해서 프로젝트 진입 CSS 파일 맨 위에 붙여넣으면 동작한다. @theme 블록과 .btn 규칙은 원하는 만큼 자유롭게 추가하면 된다.
5번 섹션에서 봤듯, packages/tailwindcss/src/compat/에는 @config "./tailwind.config.js";를 지원하는 코드가 실제로 존재한다. 기본값이 바뀐 것이지 기능이 사라진 것은 아니다. "v4는 무조건 CSS로 다시 짜야 한다"고 지레 겁먹고 마이그레이션을 미루는 경우가 있는데, 기존 설정 파일을 @config로 그대로 연결한 뒤 천천히 @theme으로 옮겨가는 점진적 전환이 가능하다.
스캐너는 소스 코드에 완전한 문자열 형태로 등장한 클래스만 찾아낸다. 그래서 다음처럼 자바스크립트 변수로 클래스 이름 일부를 조립하면 스캐너가 그 결과를 예측할 수 없어 CSS가 안 만들어진다.
/* 이렇게 조립하면 스캐너가 "bg-" + color 를 못 찾는다 */
const cls = `bg-${color}-500`
/* 완전한 클래스명을 그대로 등장시켜야 스캐너가 인식한다 */
const cls = color === 'red' ? 'bg-red-500' : 'bg-blue-500'
이건 러스트 스캐너가 "문자열 생김새"만 보고 후보를 뽑기 때문에 생기는 근본적인 한계다(5번 섹션 원리 ①). 자바스크립트를 실행해서 값을 계산하는 게 아니라, 소스 텍스트를 그대로 훑기 때문이다.
@tailwindcss/upgrade 패키지(자동 마이그레이션 도구)의 소스를 보면 LEGACY_CLASS_MAP이라는 이름 변경 표가 있다 — flex-grow→grow, flex-shrink→shrink, overflow-ellipsis→text-ellipsis, decoration-clone→box-decoration-clone 같은 단순 개명은 자동 변환이 안전하다. 하지만 딱 하나, outline-none은 자동 변환이 조건부다.
outline-none — v3와 v4에서 서로 다른 뜻v3에서 outline-none은 "포커스 시 보이는 테두리 스타일을 접근성 친화적으로 감추기"(v4의 outline-hidden과 같은 의미)였다. 그런데 v4에는 outline-none이라는 클래스가 여전히 존재하되, 이번엔 진짜로 outline-style: none만 적용하는 다른 뜻으로 재정의됐다.
업그레이드 도구 소스의 주석에는 "v3 프로젝트를 마이그레이션하는 경우에 한해서만 outline-none을 outline-hidden으로 바꿔준다"고 명시돼 있다. 즉 자동 도구를 안 돌리고 손으로 파일만 복사해 온 경우, 코드는 그대로 동작하는 것처럼 보이지만 키보드 포커스 스타일의 의미가 조용히 달라진다. v3에서 넘어온다면 반드시 공식 npx @tailwindcss/upgrade 도구를 돌리거나, outline-none을 쓴 곳을 전부 검색해 직접 확인해야 한다.
Tailwind는 모노레포 안에 tailwindcss·@tailwindcss/cli·@tailwindcss/oxide 등 여러 패키지를 따로 배포한다. 버전의 진실원(source of truth)은 각 패키지 자신의 package.json의 version 필드다 — 실제로 코어 패키지는 import { version } from '../package.json'로 이 값을 직접 읽어 컴파일된 CSS 맨 위에 /*! tailwindcss v4.3.3 | MIT License | ... */ 배너 주석으로 찍는다.
문제는 @tailwindcss/oxide의 네이티브 바이너리가 13개 OS/아키텍처별 패키지(darwin-arm64, linux-x64-gnu, win32-arm64-msvc 등)로 나뉘어 배포된다는 점이다. 저장소의 scripts/version-packages.mjs는 이 13개 패키지가 코어 @tailwindcss/oxide와 항상 같은 버전을 갖도록 syncedWorkspaces 목록으로 강제 동기화한다 — 사람이 수동으로 버전을 올리면 이 동기화가 깨질 위험이 있다는 뜻이라, 릴리스는 반드시 이 스크립트를 통해서만 이뤄진다.
package.json 자신을 그 원본으로 삼는다.Tailwind와 자주 비교되는 대안들은 사실 서로 다른 질문에 답하는 도구인 경우가 많다. "유틸리티 클래스 방식을 계속 쓰되 다른 엔진을 쓸까?"와 "유틸리티 클래스 방식 자체를 관둘까?"는 다른 결정이다.
| 대안 | 철학 차이 · 언제 고려하나 |
|---|---|
| UnoCSS | Tailwind와 같은 유틸리티 클래스 철학을 쓰지만, "미리 정의된 클래스 목록"조차 두지 않고 규칙(정규식 패턴)으로 클래스를 즉석 해석한다. "이미 v3 시절부터 최고 속도를 노렸던" 도구였으나, v4가 러스트 엔진을 들이면서 속도 격차는 좁혀졌다. Tailwind 생태계(문서·플러그인·shadcn/ui 호환)를 포기할 수 없다면 굳이 옮길 이유는 줄었다. |
| CSS Modules | 유틸리티 클래스가 아니라 "파일마다 스코프가 자동으로 분리되는 평범한 CSS"다. .card라는 이름을 그대로 쓰되, 빌드 도구가 파일별로 고유한 이름으로 바꿔줘 이름 충돌만 막아준다. "클래스를 HTML에 잔뜩 나열하는 게 싫다", "디자이너가 CSS를 직접 짜는 팀이다"라면 이쪽이 더 익숙하다. 대신 "이름 짓기 피로"(3번 섹션)는 그대로 남는다. |
| vanilla-extract | 타입스크립트 코드 자체로 스타일을 정의하고, 빌드 시점에 정적 CSS로 변환하는 "타입 안전(type-safe) CSS-in-JS"다. 디자인 토큰을 타입스크립트 객체로 정의해 자동완성·오타 검사의 도움을 받고 싶은 팀에 맞다. 런타임에 스타일을 계산하지 않는다는 점(정적 추출)은 Tailwind와 방향이 비슷하지만, "클래스 이름을 HTML에 나열"하는 대신 "스타일 객체를 임포트해서 쓴다"는 점이 근본적으로 다르다. |
| Bootstrap 계열 | 유틸리티가 아니라 완성된 컴포넌트("이 버튼", "이 카드")를 준다. 빨리 만들고 싶고 커스터마이징을 많이 안 할 프로젝트에 유리하지만, 나만의 디자인을 표현하려면 결국 그 위에 덮어써야 해서 Tailwind가 지향하는 "이름 없는 조각으로 원하는 대로 조립"과는 반대 방향이다. |
Tailwind와 UnoCSS의 차이는 "미리 인쇄된 라벨이 붙은 물감통"과 "그때그때 배합 비율을 읽어 색을 섞어주는 조색기"의 차이에 가깝다. CSS Modules·vanilla-extract는 아예 다른 매장이다 — "이름표 붙은 물감통 매대"가 아니라 "내가 직접 배합하되, 매장(빌드 도구)이 다른 손님 것과 안 섞이게만 정리해 주는" 방식이다.
Tailwind CSS의 정체를 한 문장으로 다시 요약하면 — "CSS 속성 하나하나에 미리 이름을 붙여 두고, 러스트로 짠 스캐너가 실제 쓰인 이름만 찾아 최종 CSS를 만들어주는 유틸리티 우선 프레임워크"다. v4의 핵심은 이 스캐너와 컴파일 엔진을 자바스크립트에서 러스트로 옮기고, 설정을 자바스크립트 파일에서 CSS 자체(@theme)로 옮긴 것이다.
스타트업 프로토타입, 관리자 화면, 컴포넌트를 계속 실험하는 디자인 시스템 초기 단계처럼 "빨리 만들고 자주 갈아엎는" 작업에 잘 맞는다. AI 코딩 도구와 함께 화면을 빠르게 뽑아낼 때도 궁합이 좋다(2번 섹션).
class="flex items-center justify-between gap-4 rounded-lg border border-gray-200 p-6 shadow-sm hover:shadow-md"처럼 클래스가 길어지는 걸 "가독성 저하"로 느끼는 팀도 있다. 이런 경우 CSS Modules나 @apply로 조합을 이름 붙여 재사용하는 절충안(8번 섹션)을 먼저 검토하는 게 좋다.
outline-none 사용처를 따로 확인9번 섹션에서 다룬 대로, 손으로 파일만 복사해 마이그레이션하면 outline-none의 뜻이 조용히 바뀐다. npx @tailwindcss/upgrade를 반드시 거치고, 결과 diff를 검토하는 습관을 들이는 게 안전하다.