Tailwind CSS

Tailwind CSS @apply 사용 기준: @utility와 custom variant 구분하기

2026.04.30·수정 2026.05.12·약 18분

이 글에서 정리하는 내용

Tailwind CSS v4 기준으로 를 어디에 쓰고 어디에서 멈춰야 하는지 정리합니다. 반복 버튼을 줄이는 문제, third-party 라이브러리 스타일 덮어쓰기, @utility로 프로젝트 전용 유틸리티를 만드는 기준, @custom-variantdata-theme 조건을 Tailwind 문법 안에 넣는 흐름까지 함께 봅니다.

Tailwind를 쓰다 보면 왜 가 떠오를까

Tailwind @apply 기준: @utility custom variant 차이 핵심 개념을 설명하는 첫 번째 본문 이미지

Tailwind CSS를 쓰면 처음에는 화면을 잡는 속도가 빠릅니다. 버튼 하나를 만들 때도 CSS 파일을 오가며 이름을 고민하지 않고, 필요한 유틸리티를 바로 붙이면 됩니다. px-4, py-2, rounded-lg, bg-blue-600, text-white처럼 스타일이 마크업 가까이에 남기 때문에 나중에 다시 볼 때도 요소의 모양을 대략 읽을 수 있습니다.

그런데 화면이 커지면 이 장점이 부담처럼 보이는 순간이 옵니다. 관리자 화면의 저장 버튼, 쇼핑몰 상품 카드의 구매 버튼, 필터 영역의 활성 버튼처럼 비슷한 조합이 여러 곳에 반복됩니다. 그때 className이 길어졌다는 이유로 @apply를 떠올리기 쉽습니다.

@apply는 Tailwind 유틸리티 클래스를 CSS 선택자 안에 인라인하는 지시어입니다. Tailwind를 버리고 일반 CSS로 돌아가는 문법은 아닙니다. 이미 쓰고 있는 유틸리티 조합을 CSS 파일 안에서 다시 쓰는 방식입니다.

.btn-primary { @apply rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white;
}

이렇게 작성하면 HTML에는 btn-primary만 남습니다. 짧아 보이는 건 맞습니다. 다만 이 순간부터 버튼의 실제 스타일은 HTML이 아니라 CSS 파일에 숨어 있습니다. Tailwind를 쓰면서도 다시 “이 class가 어디에 정의되어 있지?”를 추적해야 합니다.

그래서 @apply를 볼 때 첫 질문은 “이 길어졌나?”가 아닙니다. “이 스타일을 마크업에서 직접 다루는 게 어려운가?”에 더 가깝습니다. 이 질문을 건너뛰면 @apply는 정리 도구가 아니라, Tailwind의 장점을 흐리는 우회로가 됩니다.

가 필요한 상황과 그렇지 않은 상황

@apply를 판단할 때는 UI의 소유권을 먼저 나누는 게 좋습니다. 내가 직접 만든 버튼인지, 외부 라이브러리가 만들어낸 DOM인지, 또는 CSS 모듈이나 컴포넌트 내부 스타일처럼 Tailwind의 전역 context를 바로 참조하기 어려운 구조인지부터 봐야 합니다.

직접 만든 버튼, 카드, 배지라면 @apply보다 컴포넌트 분리를 먼저 검토할 수 있습니다. React나 Next.js 프로젝트에서는 버튼 스타일을 CSS class 하나로 숨기는 것보다, Button 컴포넌트에서 variant, size, disabled 같은 상태를 함께 관리하는 편이 수정 지점이 선명합니다.

<button class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"> 저장하기
</button>

이 버튼이 한두 군데에서만 쓰인다면 그대로 두는 편이 더 읽기 쉽습니다. hover 상태, 여백, 글자 크기, 배경색이 한 자리에서 보입니다. 반대로 아래처럼 줄이면 마크업은 짧아지지만 버튼의 실제 모양은 다른 파일을 열어야 알 수 있습니다.

<button class="btn-primary"> 저장하기
</button>

처음에는 큰 문제가 없어 보입니다. 하지만 버튼에 sm, lg, outline, danger, loading 같은 변형이 붙기 시작하면 .btn-primary 하나로 끝나지 않습니다. 결국 CSS 파일 안에 .btn-primary-sm, .btn-primary-danger처럼 새로운 이름이 늘어납니다. Tailwind를 쓰면서도 다시 Bootstrap식 class 이름을 설계하는 상태가 될 수 있습니다.

직접 관리하는 UI라면 기준을 이렇게 잡는 편이 낫습니다. 단순 반복은 컴포넌트로 묶고, 외부에서 만들어진 class나 직접 을 넣기 어려운 요소에만 @apply를 검토합니다. 특히 스타일과 동작이 함께 커지는 버튼, 카드, 모달, 드롭다운은 CSS class보다 컴포넌트 단위로 이름을 남기는 쪽이 오래 갑니다.

Tailwind v4에서 CSS modules, Vue나 Svelte의 component style block처럼 별도 CSS context에서 @apply@variant를 쓸 때는 @reference가 필요할 수 있습니다. 메인 stylesheet의 theme 변수, custom utility, custom variant를 참조해야 하기 때문입니다. 전역 CSS에서는 문제없던 코드가 모듈 파일로 옮겨간 뒤 깨진다면 이 지점도 같이 확인해야 합니다.

상황 먼저 볼 선택지 판단 기준
직접 만든 버튼이 반복됨 컴포넌트 분리 상태와 동작까지 함께 커질 가능성이 높음
외부 라이브러리 내부 class를 덮어써야 함 마크업에 직접 Tailwind class를 넣기 어려움
여러 화면에서 같은 CSS 동작이 필요함 Tailwind 유틸리티처럼 조합해서 쓰는 편이 자연스러움
data-theme나 data-state 조건이 반복됨 @custom-variant 조건부 class 조합보다 variant 이름으로 남기는 편이 명확함

third-party 라이브러리 스타일을 Tailwind 기준으로 맞추기

@apply가 가장 설득력 있게 쓰이는 곳은 third-party 라이브러리 스타일을 덮어쓸 때입니다. Swiper, DatePicker, Toast, Select 계열 라이브러리는 내부 DOM을 라이브러리가 만듭니다. 개발자가 모든 요소에 원하는 className을 직접 넣기 어렵습니다.

이런 경우에는 라이브러리가 제공하는 class selector를 대상으로 CSS를 작성해야 합니다. 여기서 일반 CSS 값을 직접 쓰기 시작하면 프로젝트의 색상, 간격, radius 기준이 Tailwind와 따로 움직입니다. @apply를 쓰면 외부 라이브러리 영역도 Tailwind의 디자인 토큰과 같은 문법 안에서 맞출 수 있습니다.

.swiper-pagination-bullet { @apply size-2 bg-slate-300 opacity-100;
} .swiper-pagination-bullet-active { @apply w-6 rounded-full bg-slate-900;
}

이 코드는 Swiper 페이지네이션 bullet을 Tailwind 유틸리티 기준으로 맞추는 예시입니다. 라이브러리 내부에서 만들어지는 요소라 HTML에 직접 size-2bg-slate-300을 붙일 수 없습니다. CSS 선택자로 접근해야 하고, 그 안에서 Tailwind의 값 체계를 쓰기 위해 @apply를 사용합니다.

다만 외부 라이브러리 스타일을 덮어쓸 때는 선택자 범위를 먼저 줄여야 합니다. 전역 CSS에 너무 넓은 선택자를 쓰면 프로젝트 전체의 같은 라이브러리 인스턴스가 한 번에 바뀝니다. 관리자 대시보드 안의 Swiper만 바꾸고 싶다면 부모 영역 class나 data attribute를 함께 둬야 합니다.

.admin-dashboard .swiper-pagination-bullet { @apply size-2 bg-slate-300 opacity-100;
} .admin-dashboard .swiper-pagination-bullet-active { @apply w-6 rounded-full bg-slate-900;
}

이렇게 범위를 좁히면 같은 Swiper를 이벤트 페이지나 상품 상세 페이지에서 쓰더라도 스타일 충돌이 줄어듭니다. @apply 자체보다 중요한 부분은 선택자의 경계입니다. 덮어쓰기 CSS는 편하지만, 전역에 가까워지는 순간 예상하지 못한 화면까지 영향을 줍니다.

외부 라이브러리 스타일을 손볼 때는 세 가지를 먼저 확인하면 됩니다. 첫째, 라이브러리가 어떤 class를 실제 DOM에 찍는지 확인합니다. 둘째, 그 class가 한 화면에서만 쓰이는지 프로젝트 전체에서 쓰이는지 확인합니다. 셋째, 라이브러리 기본 CSS와 내가 작성한 CSS 중 어느 쪽이 나중에 로드되는지 확인합니다. 이 세 가지가 정리되지 않으면 @apply를 써도 원하는 스타일이 적용되지 않거나, 다른 화면이 같이 바뀔 수 있습니다.

로 프로젝트 전용 유틸리티 만들기

@apply@utility는 이름만 보면 둘 다 커스텀 CSS를 정리하는 문법처럼 보입니다. 하지만 쓰임이 다릅니다. @apply는 이미 존재하는 Tailwind 유틸리티를 CSS 선택자 안으로 가져오는 방식입니다. @utility는 프로젝트에서 사용할 새 유틸리티 class를 등록하는 방식입니다.

예를 들어 스크롤바를 숨기는 패턴은 Tailwind 기본 유틸리티만으로 표현하기 애매할 수 있습니다. 이럴 때 특정 컴포넌트 class 안에 스타일을 묶어두면 그 컴포넌트에서만 의미가 생깁니다. 반대로 프로젝트 전용 유틸리티로 등록하면 Tailwind class를 조합하는 흐름 안에서 계속 사용할 수 있습니다.

@utility scrollbar-hidden { &::-webkit-scrollbar { display: none; } scrollbar-width: none;
}
<div class="scrollbar-hidden overflow-x-auto"> <div class="flex gap-3"> <button class="shrink-0 rounded-full border px-4 py-2 text-sm">전체</button> <button class="shrink-0 rounded-full border px-4 py-2 text-sm">진행중</button> <button class="shrink-0 rounded-full border px-4 py-2 text-sm">종료</button> </div>
</div>

모바일 필터처럼 가로 스크롤이 자주 필요한 UI에서는 scrollbar-hidden이 여러 화면에 반복될 수 있습니다. 이 경우 @apply.event-filter-list 같은 특정 컴포넌트 class를 만드는 것보다, 유틸리티 이름으로 남기는 편이 재사용 기준이 분명합니다.

다만 @utility도 아무 스타일이나 넣는 장소는 아닙니다. 이름이 너무 구체적이면 유틸리티가 아니라 컴포넌트 전용 class가 됩니다. admin-product-card-title 같은 이름은 특정 화면에 묶여 있습니다. 반대로 scrollbar-hidden, content-auto, tap-highlight-none처럼 CSS 동작 자체를 설명하는 이름은 여러 화면에서 같은 의미로 쓸 수 있습니다.

@utility content-auto { content-visibility: auto;
} @utility tap-highlight-none { -webkit-tap-highlight-color: transparent;
}

이런 유틸리티는 카드 목록, 긴 섹션, 모바일 버튼 영역처럼 여러 곳에 붙일 수 있습니다. 특정 디자인 모양을 숨기는 이름이 아니라 CSS 동작을 표현하는 이름이기 때문에 Tailwind의 기존 유틸리티와 함께 쓰기 쉽습니다.

@utility를 만들기 전에 확인할 질문은 하나입니다. “이 class를 다른 화면에서도 같은 의미로 쓸 수 있는가?”입니다. 대답이 어렵다면 유틸리티보다 컴포넌트나 일반 CSS class가 맞을 가능성이 높습니다.

@custom-variant로 data-theme 스타일 확장하기

Tailwind @apply 기준: @utility custom variant 차이 적용 흐름을 설명하는 두 번째 본문 이미지

Tailwind에는 이미 hover:, focus:, disabled:, dark: 같은 variant가 있습니다. 그런데 실제 프로젝트에서는 라이트와 다크만으로 상태가 끝나지 않습니다. 관리자 화면, 쇼핑몰 화면이벤트 화면처럼 같은 컴포넌트를 쓰더라도 영역별 톤이 달라져야 할 수 있습니다.

이럴 때 모든 컴포넌트에 조건부 class를 직접 넣으면 코드가 금방 복잡해집니다. 상위 영역에 data-theme를 두고, 그 값을 기준으로 Tailwind variant를 만들면 테마 조건을 class 문법 안에서 다룰 수 있습니다.

@custom-variant theme-admin (&:where([data-theme="admin"] *));
@custom-variant theme-shop (&:where([data-theme="shop"] *));
<section data-theme="admin"> <div class="rounded-xl border bg-white p-5 theme-admin:border-slate-700 theme-admin:bg-slate-950 theme-admin:text-white"> 관리자 통계 카드 </div>
</section>

이 구조에서는 data-theme="admin" 아래에 있는 요소에만 theme-admin: variant가 적용됩니다. 컴포넌트 내부에서 “지금 관리자 화면인가?”를 계속 확인하지 않아도, 상위 DOM의 테마 값이 스타일 조건이 됩니다.

같은 카드 컴포넌트를 여러 영역에서 재사용할 때 차이가 더 잘 보입니다. 기본 카드 스타일은 그대로 두고, 관리자 영역에서만 border와 배경을 어둡게 바꾸거나, 쇼핑몰 영역에서만 강조 색상을 다르게 줄 수 있습니다.

<section data-theme="shop"> <div class="rounded-xl border p-5 theme-shop:border-amber-200 theme-shop:bg-amber-50"> 상품 추천 카드 </div>
</section>

@custom-variant는 테마뿐 아니라 프로젝트에서 반복되는 상태 조건에도 쓸 수 있습니다. 예를 들어 data-state="open", data-active="true" 같은 속성을 자주 사용한다면 이를 variant로 등록해 class 조합을 단순하게 만들 수 있습니다.

@custom-variant state-open (&:where([data-state="open"] *));

variant 이름은 프로젝트 전체에 드러나는 문법입니다. 그래서 theme-admin:, theme-shop:, state-open:처럼 조건이 바로 읽히는 이름이 좋습니다. 너무 짧게 줄이면 작성할 때는 편해도, 나중에 코드를 보는 사람이 다시 의미를 추적해야 합니다.

또 하나 조심할 점은 variant가 늘어날수록 스타일 조건도 늘어난다는 점입니다. theme-admin:, theme-shop:, state-open:, state-invalid:가 한 요소에 계속 붙으면 은 다시 길어집니다. 이때는 variant를 더 만드는 문제가 아니라 컴포넌트 구조를 나눠야 하는 신호일 수 있습니다.

정리: 는 줄이는 문법이 아니라 경계를 맞추는 도구

@apply를 Tailwind class를 짧게 만드는 문법으로만 보면 금방 남발하게 됩니다. 이 길다는 이유만으로 CSS 파일에 숨기기 시작하면 Tailwind의 장점인 “마크업에서 스타일을 바로 읽는 흐름”이 약해집니다.

직접 만든 UI라면 먼저 컴포넌트 분리를 봅니다. 버튼, 카드, 배지처럼 상태와 동작이 함께 커질 수 있는 요소는 CSS class보다 컴포넌트 이름으로 남기는 쪽이 수정 위치를 찾기 쉽습니다.

외부 라이브러리처럼 내부 DOM에 직접 을 넣기 어려운 영역에서는 @apply가 맞는 선택이 될 수 있습니다. 이때는 선택자 범위를 좁히고, 프로젝트의 색상과 간격 기준을 Tailwind 유틸리티로 맞추는 데 집중해야 합니다.

프로젝트 전용 CSS 동작이 여러 곳에서 반복된다면 @utility를 검토합니다. 특정 화면의 모양이 아니라 여러 화면에서 같은 의미로 조합할 수 있는 규칙인지 확인하는 것이 기준입니다.

테마나 상태 조건이 반복된다면 @custom-variant가 맞습니다. data-themedata-state를 variant로 연결해두면, Tailwind의 기존 문법 흐름을 유지하면서 프로젝트만의 조건을 추가할 수 있습니다.

다음 작업에서 다시 볼 기준은 이렇습니다. 유틸리티는 가능한 한 마크업 가까이에 둡니다. 반복 UI는 컴포넌트로 묶습니다. 직접 제어하기 어려운 CSS 경계에서만 @apply를 씁니다. 여러 화면에서 같은 의미로 쓸 수 있는 CSS 동작은 @utility로 남깁니다. 프로젝트 전용 조건은 @custom-variant로 이름을 붙입니다. 이 구분이 잡히면 Tailwind 코드가 길어지는 문제를 무조건 숨기지 않고, 필요한 경계에서만 정리할 수 있습니다.

참고 자료

Tailwind CSS v4의 functions and directives, adding custom styles, theme variables 문서를 기준으로 @apply, @utility, @custom-variant, @reference의 역할을 확인했습니다. 실제 프로젝트에서는 사용 중인 Tailwind CSS 버전과 빌드 환경에 따라 세부 동작을 함께 확인해야 합니다.

같이 읽으면 좋은 글

이 글이 마음에 드세요?

RSS 피드를 구독하세요!

“Tailwind CSS @apply 사용 기준: @utility와 custom variant 구분하기”에 대한 5개의 생각

댓글 남기기