Tailwind CSS v4의 @custom-variant, @plugin 디렉티브, CSS 기반 확장 시스템, 그리고 기존 JS 플러그인에서의 마이그레이션을 실전 중심으로 다룹니다.
8장에서 다크 모드와 테마 전략을 배웠습니다. 이 장에서는 Tailwind CSS v4의 확장 시스템을 다룹니다. @custom-variant로 커스텀 변형을 정의하고, @plugin으로 JavaScript 플러그인을 연결하며, 프로젝트에 맞는 확장을 구축하는 방법을 살펴봅니다.
@custom-variant는 CSS 선택자를 기반으로 새로운 변형을 정의합니다.
/* 다크 모드 변형 (8장에서 다룸) */
@custom-variant dark (&:where(.dark, .dark *));
/* RTL(오른쪽에서 왼쪽) 변형 */
@custom-variant rtl (&:where([dir="rtl"], [dir="rtl"] *));
/* 인쇄 미디어 변형 */
@custom-variant print {
@media print {
&
}
}&는 실제 선택자가 대체될 위치를 나타냅니다.
<!-- RTL 레이아웃 대응 -->
<div class="ml-4 rtl:mr-4 rtl:ml-0">RTL 대응 여백</div>
<!-- 인쇄 시 스타일 -->
<nav class="flex gap-4 print:hidden">네비게이션</nav>
<article class="print:text-black print:bg-white">인쇄용 콘텐츠</article>/* 모션 선호도 */
@custom-variant motion-safe {
@media (prefers-reduced-motion: no-preference) {
&
}
}
@custom-variant motion-reduce {
@media (prefers-reduced-motion: reduce) {
&
}
}
/* 고대비 모드 */
@custom-variant high-contrast {
@media (prefers-contrast: more) {
&
}
}
/* 가로/세로 방향 */
@custom-variant landscape {
@media (orientation: landscape) {
&
}
}
@custom-variant portrait {
@media (orientation: portrait) {
&
}
}/* 로딩 상태 */
@custom-variant loading (&:where([data-loading], [data-loading] *));
/* 인증 상태 */
@custom-variant authenticated (&:where([data-authenticated], [data-authenticated] *));
/* 에디터 모드 */
@custom-variant editing (&:where([data-editing], [data-editing] *));<body data-loading>
<div class="loading:opacity-50 loading:pointer-events-none">
로딩 중 비활성화
</div>
<div class="loading:block hidden">
로딩 스피너
</div>
</body>
<main data-editing>
<div class="editing:ring-2 editing:ring-primary editing:ring-dashed">
편집 모드에서 테두리 표시
</div>
</main>커스텀 변형은 프로젝트의 상태 관리 패턴과 긴밀하게 연결됩니다. data-* 속성을 상태 표현에 사용하면, JavaScript의 상태 변경이 CSS 변형으로 자연스럽게 반영됩니다. React에서는 data-loading={isLoading || undefined}처럼 조건부로 속성을 추가합니다.
여러 조건을 결합한 변형도 정의할 수 있습니다.
/* 다크 모드 + 고대비 */
@custom-variant dark-high-contrast {
@media (prefers-contrast: more) {
&:where(.dark, .dark *)
}
}
/* 터치 디바이스 */
@custom-variant touch {
@media (hover: none) and (pointer: coarse) {
&
}
}
/* 마우스 디바이스 */
@custom-variant mouse {
@media (hover: hover) and (pointer: fine) {
&
}
}<!-- 터치에서는 더 큰 터치 타겟 -->
<button class="p-2 touch:p-4 touch:text-lg">
클릭/탭
</button>
<!-- 마우스에서만 hover 효과 -->
<div class="mouse:hover:shadow-lg mouse:hover:scale-105 transition">
카드
</div>CSS만으로 해결하기 어려운 복잡한 확장은 @plugin 디렉티브로 JavaScript 플러그인을 연결합니다.
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";
@plugin "./plugins/custom-components.js";산문(prose) 콘텐츠에 타이포그래피 스타일을 적용합니다.
@plugin "@tailwindcss/typography";<article class="prose prose-lg dark:prose-invert max-w-none">
<h1>블로그 포스트 제목</h1>
<p>마크다운이나 CMS에서 생성된 HTML 콘텐츠에
자동으로 타이포그래피 스타일이 적용됩니다.</p>
<pre><code>console.log('코드 블록도 스타일링됩니다');</code></pre>
</article>폼 요소의 기본 스타일을 개선합니다.
@plugin "@tailwindcss/forms";<input type="text" class="rounded-md border-border" placeholder="텍스트 입력" />
<select class="rounded-md border-border">
<option>옵션 1</option>
<option>옵션 2</option>
</select>
<input type="checkbox" class="rounded text-primary" />v4에서도 JavaScript 플러그인 API는 여전히 사용 가능합니다.
import plugin from 'tailwindcss/plugin';
export default plugin(function ({ addComponents, addUtilities, theme }) {
addComponents({
'.skeleton': {
'background': `linear-gradient(90deg,
${theme('colors.neutral.200')} 25%,
${theme('colors.neutral.300')} 50%,
${theme('colors.neutral.200')} 75%)`,
'background-size': '200% 100%',
'animation': 'skeleton-loading 1.5s ease-in-out infinite',
'border-radius': theme('borderRadius.md'),
},
});
addUtilities({
'.scrollbar-thin': {
'scrollbar-width': 'thin',
},
'.scrollbar-none': {
'scrollbar-width': 'none',
'&::-webkit-scrollbar': {
'display': 'none',
},
},
});
});@import "tailwindcss";
@plugin "./plugins/custom-components.js";v4의 플러그인 API는 v3과 대부분 호환됩니다. addBase(), addComponents(), addUtilities(), matchUtilities() 등의 함수를 동일하게 사용할 수 있습니다. 다만, 간단한 확장은 CSS의 @utility나 @layer를 사용하는 것이 더 간결합니다.
| 상황 | 권장 방식 |
|---|---|
| 단순 유틸리티 클래스 추가 | @utility (CSS) |
| 커스텀 변형 정의 | @custom-variant (CSS) |
| 정적 컴포넌트 스타일 | @layer components (CSS) |
| 다크 모드/테마 변형 | @custom-variant (CSS) |
| 동적 값 기반 유틸리티 | matchUtilities() (JS Plugin) |
| 복잡한 기본 스타일 (typography) | @plugin (JS Plugin) |
| 서드파티 플러그인 사용 | @plugin (JS Plugin) |
/* 대부분의 확장은 CSS로 충분 */
@import "tailwindcss";
/* 커스텀 변형 */
@custom-variant dark (&:where(.dark, .dark *));
@custom-variant rtl (&:where([dir="rtl"], [dir="rtl"] *));
/* 커스텀 유틸리티 */
@utility scrollbar-hidden {
scrollbar-width: none;
&::-webkit-scrollbar { display: none; }
}
@utility text-balance {
text-wrap: balance;
}
/* 커스텀 컴포넌트 */
@layer components {
.btn { /* ... */ }
.card { /* ... */ }
}
/* 복잡한 플러그인만 JS로 */
@plugin "@tailwindcss/typography";여러 프로젝트에서 공유할 설정을 프리셋으로 패키징할 수 있습니다.
@theme {
--color-brand: oklch(0.60 0.22 250);
--color-brand-light: oklch(0.80 0.12 250);
--color-brand-dark: oklch(0.40 0.24 252);
--font-sans: "Pretendard Variable", system-ui, sans-serif;
--font-mono: "JetBrains Mono", monospace;
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
}
@custom-variant dark (&:where(.dark, .dark *));
@utility text-balance {
text-wrap: balance;
}@import "tailwindcss";
@import "./preset.css";
/* 프로젝트별 추가 설정 */
@theme {
--color-accent: oklch(0.70 0.20 330);
}Tailwind CSS v4의 확장 시스템은 CSS 우선(CSS-first) 철학을 따릅니다. @custom-variant로 커스텀 변형을, @utility로 커스텀 유틸리티를, @layer components로 컴포넌트 스타일을 정의합니다. 복잡한 동적 확장이 필요한 경우에만 @plugin으로 JavaScript 플러그인을 연결합니다. 이 접근법은 도구 체인을 단순화하고, CSS 도구와의 호환성을 최대화합니다.
다음 장에서는 지금까지 배운 모든 기능을 종합하여 실전 디자인 시스템을 구축합니다. 토큰 설계, 컴포넌트 라이브러리, 테마 시스템, 그리고 팀 공유를 위한 구조화 전략을 다룹니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
Tailwind CSS v4로 프로덕션 디자인 시스템을 구축합니다. 토큰 계층 설계, 컴포넌트 라이브러리, 테마 시스템, 접근성, 팀 협업 전략을 실전 중심으로 다룹니다.
Tailwind CSS v4의 다크 모드 전략, CSS 커스텀 프로퍼티 기반 런타임 테마 전환, 다중 테마 패턴, 그리고 next-themes와의 통합을 실전 중심으로 다룹니다.
Tailwind CSS v3에서 v4로의 마이그레이션 가이드 — 주요 변경사항, 자동 마이그레이션 도구, 단계별 전환 전략, 그리고 흔한 문제 해결법을 다룹니다.