prettier-plugin-tailwindcss
Tailwind CSS를 쓰다 보면 클래스가 길어지는 건 피할 수 없다. 문제는 길이가 아니라 순서다. 같은 스타일을 적용하더라도 사람마다 클래스를 나열하는 순서가 다르다.
<!-- A가 쓴 코드 -->
<button class="text-white px-4 sm:px-8 py-2 bg-sky-700 hover:bg-sky-800">
<!-- B가 쓴 코드 -->
<button class="bg-sky-700 hover:bg-sky-800 px-4 py-2 sm:px-8 text-white">
두 코드는 완전히 같은 결과를 만든다. 하지만 diff에는 변경으로 잡히고, 코드 리뷰에서 "이거 왜 순서 바꿨어?"라는 불필요한 대화가 생긴다. ESLint 규칙으로 알파벳순 정렬을 강제하는 방법도 있지만, 알파벳순은 의미 없는 정렬이다. bg-sky-700과 border-2가 나란히 있는 게 flex와 font-bold가 나란히 있는 것보다 가독성이 좋진 않다.
prettier-plugin-tailwindcss는 Tailwind Labs에서 만든 공식 Prettier 플러그인이다. 클래스 속성 안의 Tailwind 클래스를 자동으로 정렬해준다. 핵심은 "알파벳순"이 아니라 "CSS가 실제로 적용되는 순서"로 정렬한다는 점이다.
설치와 설정
Prettier가 이미 설치되어 있다면 플러그인만 추가하면 된다.
npm install -D prettier-plugin-tailwindcss
Prettier 설정 파일에 플러그인을 등록한다.
// .prettierrc
{
"plugins": ["prettier-plugin-tailwindcss"]
}
JavaScript 설정 파일을 사용한다면 타입 힌트도 붙일 수 있다.
// prettier.config.js
/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */
export default {
plugins: ["prettier-plugin-tailwindcss"],
};
이게 끝이다. 별도 설정 없이 저장할 때마다 클래스가 자동 정렬된다.
정렬 기준
이 플러그인의 정렬 로직은 "Tailwind가 CSS를 생성하는 순서"를 그대로 따른다. Tailwind의 CSS 출력은 base → components → utilities 레이어 순서로 나오는데, 클래스 정렬도 이 순서를 반영한다.
레이어 순서
<!-- container는 component 레이어, mx-auto와 px-6은 utility 레이어 -->
<div class="container mx-auto px-6">
component 클래스가 utility 클래스보다 앞에 온다. CSS에서도 component가 먼저 선언되고 utility가 나중에 선언되어 덮어쓰기 때문에, 코드상의 순서와 실제 적용 순서가 일치한다.
박스 모델 기반 정렬
utility 클래스 내부에서는 박스 모델을 기반으로 정렬된다. 레이아웃에 영향을 크게 미치는 클래스가 앞에, 장식적인 클래스가 뒤에 온다.
<!-- 정렬 전 -->
<div class="text-gray-700 shadow-md p-3 border-gray-300 ml-4 h-24 flex border-2">
<!-- 정렬 후 -->
<div class="ml-4 flex h-24 border-2 border-gray-300 p-3 text-gray-700 shadow-md">
대략적인 순서를 정리하면 이렇다.
- 레이아웃 —
flex,grid,block,hidden - 포지셔닝 —
relative,absolute,top-0,z-10 - 크기 —
w-full,h-24,min-h-screen - 마진/패딩 —
m-4,p-3,mx-auto - 보더 —
border-2,border-gray-300,rounded-lg - 배경 —
bg-white,bg-gradient-to-r - 텍스트 —
text-lg,font-bold,text-gray-700 - 효과 —
shadow-md,opacity-50,transition
이 순서가 합리적인 이유가 있다. 코드를 읽을 때 "이 요소가 어떻게 배치되는지"를 먼저 파악하고, 그 다음 "어떻게 생겼는지"를 보는 게 자연스럽다. flex인지 grid인지를 먼저 알아야 나머지 스타일이 의미가 있다.
오버라이드 순서 보장
CSS에서 같은 속성을 가진 클래스가 여러 개 있으면, 나중에 선언된 클래스가 이긴다. 이 플러그인은 이 규칙을 반영해서, 더 구체적인 클래스가 뒤에 오도록 정렬한다.
<!-- 정렬 전: p-4가 pt-2를 덮어쓸 수 있다 -->
<div class="pt-2 p-4">
<!-- 정렬 후: p-4가 먼저, pt-2가 나중에 → pt-2가 이긴다 -->
<div class="p-4 pt-2">
p-4는 모든 방향 패딩을 설정하고, pt-2는 상단 패딩만 덮어쓴다. 정렬 후에는 pt-2가 뒤에 오기 때문에 코드 순서만 보고도 "상단 패딩은 2"라는 걸 바로 알 수 있다.
모디파이어 그룹핑
hover:, focus: 같은 상태 모디파이어가 붙은 클래스는 일반 클래스 뒤에 그룹으로 묶인다.
<!-- 정렬 전 -->
<div class="hover:opacity-75 opacity-50 hover:scale-150 scale-125">
<!-- 정렬 후 -->
<div class="scale-125 opacity-50 hover:scale-150 hover:opacity-75">
반응형 모디파이어(sm:, md:, lg:)도 마찬가지로 뒤쪽에 그룹화되며, 작은 브레이크포인트부터 큰 브레이크포인트 순서로 정렬된다.
<!-- 정렬 전 -->
<div class="lg:grid-cols-4 grid sm:grid-cols-3 grid-cols-2">
<!-- 정렬 후 -->
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4">
모바일 → 태블릿 → 데스크탑 순서로 읽을 수 있어서 반응형 디자인의 의도가 한눈에 보인다.
커스텀 클래스 처리
Tailwind 클래스가 아닌 커스텀 클래스(서드파티 라이브러리 등)는 항상 맨 앞에 놓인다.
<!-- 정렬 전 -->
<div class="p-3 shadow-xl select2-dropdown">
<!-- 정렬 후 -->
<div class="select2-dropdown p-3 shadow-xl">
커스텀 클래스가 앞에 있으면 "이 요소에 특별한 클래스가 적용되어 있다"는 걸 바로 인식할 수 있다.
커스텀 속성 정렬
기본적으로 class, className, :class, [ngClass] 속성과 @apply 디렉티브 안의 클래스를 정렬한다. 다른 속성에서도 정렬이 필요하면 tailwindAttributes 옵션을 사용한다.
{
"tailwindAttributes": ["myClassList"]
}
<button myClassList="rounded bg-blue-500 px-4 py-2 text-base text-white">
{children}
</button>
정규식 패턴도 지원한다. data- 접두사가 붙은 모든 속성을 한 번에 정렬하고 싶다면:
{
"tailwindAttributes": ["/data-.*/"]
}
함수 호출 내 클래스 정렬
clsx, cva, cn 같은 유틸리티 함수 안에 들어가는 클래스도 정렬할 수 있다. tailwindFunctions 옵션을 사용한다.
{
"tailwindFunctions": ["clsx", "cn", "cva"]
}
import clsx from "clsx";
function MyButton({ isActive, children }) {
let classes = clsx(
"rounded bg-blue-500 px-4 py-2 text-base text-white",
{
"bg-blue-700 text-gray-100": isActive,
}
);
return <button className={classes}>{children}</button>;
}
이 설정이 없으면 className에 직접 쓴 클래스만 정렬되고, 함수 안의 문자열은 그대로 남는다. clsx나 cn을 쓰고 있다면 반드시 설정해야 한다.
Tagged Template Literal
템플릿 리터럴에서도 같은 방식으로 동작한다. React Native에서 twrnc 같은 라이브러리를 사용할 때 유용하다.
{
"tailwindFunctions": ["tw"]
}
import tw from "twrnc";
function MyScreen() {
return (
<View style={tw`bg-white p-4 dark:bg-black`}>
<Text style={tw`text-md text-black dark:text-white`}>Hello</Text>
</View>
);
}
Tailwind v3 vs v4 설정 차이
v3: JavaScript 설정 파일
v3에서는 tailwind.config.js를 자동으로 찾는다. Prettier 설정 파일과 같은 디렉토리에 있으면 별도 설정이 필요 없다. 다른 위치에 있다면 경로를 지정한다.
{
"tailwindConfig": "./styles/tailwind.config.js"
}
커스텀 테마나 플러그인을 사용하고 있다면 이 설정이 중요하다. 플러그인이 config 파일을 읽어서 커스텀 유틸리티의 정렬 순서도 올바르게 처리한다.
v4: CSS 엔트리포인트
v4에서는 설정이 CSS 파일로 이동했기 때문에 tailwindStylesheet 옵션을 사용한다.
{
"tailwindStylesheet": "./src/app.css"
}
공백과 중복 처리
불필요한 공백 제거
기본적으로 클래스 사이의 불필요한 공백을 자동으로 제거한다.
<!-- 정렬 전 -->
<div class=" flex items-center gap-4 ">
<!-- 정렬 후 -->
<div class="flex items-center gap-4">
이 동작을 끄고 싶으면 tailwindPreserveWhitespace 옵션을 사용한다.
{
"tailwindPreserveWhitespace": true
}
중복 클래스 제거
같은 클래스가 두 번 쓰여 있으면 자동으로 제거한다. 하지만 Blade나 Fluid 같은 템플릿 엔진에서는 조건부 렌더링 구문이 중복으로 인식될 수 있어서 문제가 생길 수 있다.
{
"tailwindPreserveDuplicates": true
}
<!-- Fluid 템플릿: 조건부 클래스가 중복으로 잘못 인식될 수 있음 -->
<div class="
{f:if(condition: isCompact, then: 'grid-cols-3', else: 'grid-cols-5')}
{f:if(condition: isDark, then: 'bg-black/50', else: 'bg-white/50')}
grid gap-4 p-4
">
다른 Prettier 플러그인과 함께 사용
이 플러그인은 Prettier의 내부 API를 사용하는데, 해당 API는 한 번에 하나의 플러그인만 사용할 수 있다. 그래서 Tailwind Labs에서 주요 플러그인들과의 호환성을 직접 구현해놓았다.
호환되는 플러그인 목록:
@trivago/prettier-plugin-sort-imports— import 정렬@ianvs/prettier-plugin-sort-imports— import 정렬 (포크)prettier-plugin-organize-imports— import 정렬prettier-plugin-svelte— Svelte 지원prettier-plugin-astro— Astro 지원prettier-plugin-css-order— CSS 속성 순서prettier-plugin-jsdoc— JSDoc 정렬
중요한 제약사항이 있다. prettier-plugin-tailwindcss는 반드시 플러그인 배열의 마지막에 와야 한다.
{
"plugins": [
"@trivago/prettier-plugin-sort-imports",
"prettier-plugin-tailwindcss"
]
}
순서가 바뀌면 다른 플러그인이 제대로 동작하지 않을 수 있다. 이건 Prettier의 플러그인 로딩 메커니즘 때문인데, 마지막에 등록된 플러그인이 파서를 제어하면서 이전 플러그인들을 체인으로 호출하는 구조이기 때문이다.
Public API
v0.6부터 Prettier 없이도 정렬 로직만 따로 사용할 수 있는 API가 제공된다. CI/CD 파이프라인에서 클래스 순서를 검증하거나, 에디터 확장을 만들 때 유용하다.
import { createSorter } from "prettier-plugin-tailwindcss/sorter";
const sorter = await createSorter({
base: "/path/to/project",
stylesheetPath: "./app.css", // v4
});
// 공백으로 구분된 클래스 문자열 정렬
const sorted = sorter.sortClassAttributes([
"sm:bg-red-500 bg-blue-500",
"p-4 m-2",
]);
// ['bg-blue-500 sm:bg-red-500', 'm-2 p-4']
// 클래스 이름 배열 정렬
const sortedLists = sorter.sortClassLists([
["sm:bg-red-500", "bg-blue-500"],
["p-4", "m-2"],
]);
// [['bg-blue-500', 'sm:bg-red-500'], ['m-2', 'p-4']]
createSorter의 주요 옵션:
| 옵션 | 설명 |
|---|---|
base | 상대 경로 해석의 기준 디렉토리 |
configPath | Tailwind v3 설정 파일 경로 |
stylesheetPath | Tailwind v4 CSS 파일 경로 |
preserveWhitespace | 공백 유지 여부 (기본: false) |
preserveDuplicates | 중복 클래스 유지 여부 (기본: false) |
ESLint와의 차이
eslint-plugin-tailwindcss에도 classnames-order 규칙이 있다. 그런데 왜 Prettier 플러그인을 따로 쓸까?
역할이 다르다. ESLint는 코드의 "문제"를 찾는 도구이고, Prettier는 코드의 "스타일"을 통일하는 도구다. 클래스 순서는 코드 품질 문제가 아니라 스타일 문제이기 때문에 Prettier가 더 적합하다.
실행 시점도 다르다. Prettier는 파일을 저장할 때마다 자동으로 실행되고, ESLint는 보통 커밋이나 CI에서 실행된다. 저장할 때마다 정렬되는 게 개발 경험이 훨씬 좋다.
호환성도 더 좋다. Prettier 플러그인은 Tailwind의 설정 파일을 직접 읽어서 커스텀 유틸리티까지 올바르게 정렬한다. ESLint 플러그인은 별도의 설정이 필요하고, 커스텀 유틸리티 지원이 제한적일 수 있다.
둘 다 사용하는 프로젝트도 있다. ESLint로는 no-custom-classname 같은 규칙을 적용하고, 클래스 정렬은 Prettier에 맡기는 조합이다.
정리
- CSS 적용 순서 기반 정렬이라 코드상의 클래스 순서와 실제 우선순위가 일치하고, 오버라이드 관계를 눈으로 바로 확인할 수 있다.
tailwindFunctions에 clsx/cn/cva를 등록해야 함수 안의 클래스 문자열까지 정렬되고, 다른 Prettier 플러그인과 함께 쓸 때는 반드시 배열 마지막에 둬야 한다.- v4에서는
tailwindStylesheet로 CSS 엔트리포인트를 지정하고, Public API로 Prettier 없이 정렬 로직만 따로 사용할 수도 있다.