이 글에서 정리하는 내용
Tailwind CSS v4 기준으로 클래스가 맞는데도 스타일이 적용되지 않는 상황을 자동 콘텐츠 감지와 @source 관점에서 정리합니다. 컴포넌트 위치, 모노레포 공용 UI 패키지, 외부 라이브러리, CMS나 JSON 기반 클래스처럼 Tailwind가 놓치기 쉬운 지점을 실제 확인 순서 중심으로 다룹니다.
- 클래스는 맞는데 스타일이 안 먹는 상황
- Tailwind v4 자동 콘텐츠 감지 기준
@source가 필요한 경우- 무조건
@source를 추가하면 안 되는 경우 - 동적 클래스명 문제와 감지 누락 구분
- CMS, JSON, 생성 파일에서 클래스가 나오는 경우
- 실제로 확인할 순서
- 정리
클래스는 맞는데 스타일이 안 먹는 상황

Tailwind를 쓰다 보면 클래스 이름은 맞는데 화면에서는 스타일이 빠지는 순간이 있습니다. 버튼 컴포넌트에 bg-blue-600, hover:bg-blue-700, rounded-lg를 넣었는데 실제 화면에서는 배경색만 빠지거나 hover 스타일이 생성되지 않는 식입니다.
처음에는 오타, CSS 우선순위, 개발 서버 캐시를 의심하게 됩니다. 그 가능성도 확인해야 하지만, Tailwind에서는 질문의 방향을 조금 바꿔야 합니다. 지금 작성한 클래스가 들어 있는 파일을 Tailwind가 실제로 보고 있는지부터 확인해야 합니다.
Tailwind는 모든 유틸리티 CSS를 미리 통째로 만들어두는 방식이 아닙니다. 프로젝트 안의 파일에서 클래스처럼 보이는 문자열을 찾고, 발견한 클래스에 맞는 CSS를 생성합니다. 그래서 클래스 이름 자체가 맞아도 Tailwind가 그 파일을 스캔하지 못하면 결과 CSS에는 해당 클래스가 들어가지 않습니다.
카드 목록이나 대시보드처럼 레이아웃 클래스가 많은 화면에서는 이 차이가 더 빨리 드러납니다. grid-cols-3, gap-6, col-span-2 같은 클래스가 일부만 빠지는 상황이라면 Tailwind CSS Grid 실무 레이아웃 정리처럼 레이아웃 클래스 자체를 점검하는 흐름과 함께, 해당 컴포넌트 파일이 감지 대상인지도 같이 봐야 합니다.
Tailwind v4 자동 콘텐츠 감지 기준
Tailwind CSS v4에서는 자동 콘텐츠 감지가 강화되었습니다. v3에서 자주 작성하던 content 배열 중심 설정을 매번 직접 구성하지 않아도 되는 방향으로 바뀌었고, 일반적인 프로젝트 구조에서는 별도 설정 없이도 템플릿 파일을 찾아 유틸리티 클래스를 감지합니다.
다만 자동 감지는 프로젝트의 모든 파일을 무조건 훑는 기능이 아닙니다. Tailwind는 성능과 불필요한 CSS 생성을 줄이기 위해 기본적으로 감지하지 않는 위치를 둡니다. 예를 들어 node_modules 내부, .gitignore에 포함된 파일이미지나 zip 같은 바이너리 파일, CSS 파일, 패키지 lock 파일 등은 기본 감지 대상에서 빠집니다.
이 기준 때문에 일반적인 src 안의 React 컴포넌트는 문제없이 감지되지만, 프로젝트 바깥의 공용 패키지나 생성 파일에 있는 클래스는 놓칠 수 있습니다. 자동 감지가 편해졌다고 해서 파일 위치 점검이 사라진 것은 아닙니다.
v3의 content 설정과 v4의 감지 흐름
module.exports = { content: [ "./src/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}"], theme: { extend: {}}}; v3에서는 위처럼 어떤 파일을 스캔할지 직접 적는 일이 많았습니다. v4에서는 CSS 중심 설정 흐름으로 바뀌면서 기본적인 감지는 Tailwind가 처리합니다. 대신 자동 기준에서 벗어난 위치를 직접 알려줘야 할 때 @source가 등장합니다.
@source가 필요한 경우
@source는 Tailwind가 자동으로 찾지 못하는 소스 경로를 명시적으로 등록할 때 사용합니다. 보통 앱 코드가 아니라 앱 바깥의 UI 코드, 외부 패키지, 모노레포의 공유 패키지, 생성 파일처럼 일반적인 스캔 흐름 밖에 있는 위치가 대상입니다.
@import "tailwindcss"; @source "../node_modules/@my-company/ui-lib"; 위 코드는 회사 내부 UI 라이브러리 안에 Tailwind 클래스가 들어 있고, 앱 쪽 CSS 빌드가 그 라이브러리의 클래스까지 감지해야 하는 상황을 가정한 예시입니다. 라이브러리가 실제로 Tailwind 유틸리티 클래스를 포함하고 있는데 결과 CSS에 해당 클래스가 없다면, 앱의 스타일시트에서 해당 경로를 @source로 등록하는 식으로 접근할 수 있습니다.
모노레포에서는 경로가 조금 달라집니다. 앱은 apps/web에 있고 공용 버튼은 packages/ui에 있는 구조라면, 앱의 Tailwind CSS가 packages/ui까지 볼 수 있어야 합니다.
@import "tailwindcss"; @source "../../packages/ui"; 여기서 기준은 감지 범위를 필요한 만큼만 여는 것입니다. @source를 넓게 추가하면 Tailwind가 확인해야 할 파일도 늘어납니다. 공용 UI 패키지처럼 실제로 Tailwind 클래스가 존재하는 경로만 지정해야 나중에 빌드 흐름을 추적하기 수월합니다.
상태 스타일링에서도 같은 문제가 보일 수 있습니다. group-hover, peer-invalid, has-checked 같은 클래스가 공용 컴포넌트 안에 있는데 앱에서 생성되지 않는다면, 먼저 Tailwind CSS group peer has 차이처럼 상태 variant 구조가 맞는지 보고, 그다음 해당 파일이 감지 대상인지 확인하는 순서가 덜 흔들립니다.
무조건 @source를 추가하면 안 되는 경우
스타일이 안 먹는다고 해서 항상 @source가 답은 아닙니다. 같은 Tailwind 문제처럼 보여도 원인은 여러 갈래입니다. 커스텀 색상 클래스가 안 먹는다면 감지 누락이 아니라 @theme에 색상 토큰이 정의되지 않은 문제일 수 있습니다.
@import "tailwindcss"; @theme { --color-brand-600: #2563eb;
} bg-brand-600을 사용하려면 Tailwind가 감지할 클래스 문자열도 필요하지만, 동시에 brand-600이라는 토큰이 Tailwind 테마 안에서 해석될 수 있어야 합니다. 이쪽은 Tailwind @theme 사용법: 변수와 디자인 토큰 차이와 연결해서 보면 원인을 더 빨리 좁힐 수 있습니다.
@apply, @utility, @custom-variant와 @source를 한데 묶어 이해해도 판단이 흐려집니다. @source는 어디를 스캔할 것인지에 가까운 지시어이고, @utility나 @custom-variant는 어떤 유틸리티나 variant를 정의할 것인지에 더 가깝습니다. 역할이 다르기 때문에 Tailwind @apply 기준: @utility custom variant 차이를 함께 보면 지시어별 책임을 분리할 수 있습니다.
동적 클래스명 문제와 감지 누락 구분
Tailwind 감지 문제에서 가장 많이 섞이는 지점이 동적 클래스명입니다. 파일은 Tailwind가 보고 있는데도, 클래스가 런타임 문자열 조합으로만 존재하면 Tailwind는 완성된 클래스명을 찾지 못합니다.
function Badge({ color }: { color: "blue" | "red" }) { return ( <span className={`rounded-full px-3 py-1 text-sm bg-${color}-100 text-${color}-700`}> 상태 </span> );
} 이 코드는 React 입장에서는 문자열을 만들 수 있습니다. 하지만 Tailwind는 소스 파일을 실행해서 bg-blue-100이나 text-red-700을 계산하지 않습니다. 소스에 완성된 클래스 문자열이 없으면 CSS 생성 대상에서 빠질 수 있습니다.
이 경우에는 @source를 추가하는 것보다 클래스 조합 방식을 바꾸는 쪽이 맞습니다. Tailwind가 정적으로 읽을 수 있도록 가능한 클래스명을 객체에 완성된 문자열로 남겨야 합니다.
const badgeVariants = { blue: "bg-blue-100 text-blue-700", red: "bg-red-100 text-red-700"}; type BadgeColor = keyof typeof badgeVariants; function Badge({ color }: { color: BadgeColor }) { return ( <span className={`rounded-full px-3 py-1 text-sm ${badgeVariants[color]}`}> 상태 </span> );
} 이렇게 작성하면 bg-blue-100, text-blue-700, bg-red-100, text-red-700이 소스 안에 완성된 문자열로 남습니다. Tailwind가 파일을 스캔할 때 클래스 후보를 찾을 수 있고, 나중에 variant가 늘어나도 어떤 상태가 어떤 클래스로 연결되는지 확인할 수 있습니다.
대괄호 문법도 여기서 같이 헷갈립니다. bg-[var(--card-bg)]처럼 완성된 클래스가 소스 안에 있으면 Tailwind가 감지할 수 있지만, `bg-[${color}]`처럼 문자열 조합으로만 만들어지면 감지 기준이 흔들립니다. 이 차이는 Tailwind Arbitrary Value Variant 차이와 연결해서 정리하면 좋습니다.
CMS, JSON, 생성 파일에서 클래스가 나오는 경우
CMS나 JSON에서 클래스명을 가져오는 구조도 따로 봐야 합니다. 관리자에서 카드 색상을 선택하고, 저장된 JSON 값에 따라 bg-emerald-100, bg-rose-100 같은 클래스를 화면에 붙이는 구조라면 Tailwind가 실제 소스 파일에서 그 문자열을 못 볼 수 있습니다.
{ "cardTone": "bg-emerald-100 text-emerald-800"
} 이 JSON 파일이 Tailwind 감지 대상 안에 있고, 클래스가 완성된 문자열로 존재한다면 문제가 없을 수 있습니다. 하지만 원격 CMS 응답처럼 빌드 시점에 파일로 존재하지 않거나, 생성 파일이 .gitignore에 묶여 감지되지 않는다면 다른 방식이 필요합니다.
이때 선택지는 두 가지입니다. 실제로 사용하는 클래스 후보를 소스 코드 안에 정적 매핑으로 남기거나, Tailwind v4의 @source inline()처럼 특정 유틸리티를 명시적으로 생성하도록 지정합니다. 다만 @source inline()은 범위를 좁혀 써야 합니다. CMS에서 올 수 있는 값을 전부 무제한으로 열어두는 식으로 접근하면 나중에 CSS 크기와 관리 기준이 흐려집니다.
@import "tailwindcss"; @source inline("bg-emerald-100 text-emerald-800 bg-rose-100 text-rose-800"); 관리자 화면이나 콘텐츠 편집기에서 색상 옵션을 몇 개로 제한하는 구조라면, 코드 안의 variant map이나 @source inline()처럼 명시적인 후보 목록을 두는 쪽이 디버깅 기준을 남기기 좋습니다. 반대로 사용자가 임의로 Tailwind 클래스를 직접 입력하게 하는 방식은 검증과 유지보수 비용이 커집니다.
실제로 확인할 순서

Tailwind 스타일이 빠졌을 때는 해결 방법을 바로 고르기보다 확인 순서를 고정해두는 편이 낫습니다. 원인이 감지 누락인지, 동적 클래스명인지, 테마 토큰 문제인지 섞이면 같은 문제를 여러 번 반복하게 됩니다.
| 확인 항목 | 볼 내용 | 가능한 조치 |
|---|---|---|
| 완성된 클래스 문자열 | bg-blue-600처럼 실제 문자열이 소스에 있는가 | 동적 조합이면 variant map으로 변경 |
| 파일 위치 | 컴포넌트가 Tailwind 감지 범위 안에 있는가 | 외부 경로라면 @source 검토 |
| 제외 경로 | node_modules, .gitignore, 생성 파일에 해당하는가 | 필요한 경로만 명시적으로 등록 |
| 토큰 정의 | brand, card 같은 커스텀 값이 정의되어 있는가 | @theme 또는 테마 설정 확인 |
| variant 구조 | hover:, group-hover:, data-* 조건이 맞는가 | 상태가 발생하는 요소와 바뀌는 요소 관계 확인 |
실무에서는 이 순서만 지켜도 시간을 줄일 수 있습니다. 특히 클래스가 틀린 상황과 Tailwind가 클래스를 보지 못한 상황을 분리해야 합니다. 전자는 클래스명, variant, 테마 토큰을 확인해야 하고, 후자는 파일 위치와 스캔 범위를 확인해야 합니다.
Next.js나 Vite 프로젝트에서 개발 서버를 켜둔 상태로 경로 설정을 바꿨다면 서버를 다시 실행하는 것도 같이 확인합니다. Tailwind 설정과 감지 범위는 빌드 도구와 연결되어 있기 때문에, 파일 이동이나 패키지 경로 변경 직후에는 기존 dev server 상태만 보고 판단하지 않는 것이 좋습니다.
정리
Tailwind v4의 자동 콘텐츠 감지는 설정 부담을 줄여주지만, 스타일 누락을 완전히 없애주는 기능은 아닙니다. 클래스가 맞는데도 CSS가 생성되지 않는다면 먼저 Tailwind가 그 클래스 문자열을 볼 수 있는 위치에 있는지 확인해야 합니다.
@source는 이때 사용하는 도구입니다. 공용 UI 패키지, 모노레포 패키지, 기본 감지에서 제외된 외부 라이브러리처럼 Tailwind가 자동으로 찾기 어려운 경로를 명시적으로 열어주는 역할을 합니다. 반대로 동적 클래스명, 테마 토큰 누락, 상태 variant 구조 오류는 @source만으로 해결되지 않습니다.
다음에 Tailwind 스타일이 안 먹는 상황을 만나면 순서를 이렇게 잡으면 됩니다. 먼저 완성된 클래스 문자열이 소스 안에 있는지 보고, 그 파일이 감지 대상인지 확인합니다. 그다음 외부 경로나 제외 경로 문제라면 @source를 추가하고, 클래스가 런타임에 조합되는 구조라면 정적 매핑으로 바꿉니다. 이 기준을 잡아두면 v4의 자동 감지 흐름에서도 문제 원인을 좁게 추적할 수 있습니다.
참고 자료
Tailwind CSS 공식 문서의 source file detection, functions and directives, Tailwind CSS v4 발표 내용, Tailwind CSS v4.1의 @source inline() 설명을 기준으로 작성했습니다.
“Tailwind CSS @source 사용법: 클래스 감지 누락 해결 기준”에 대한 1개의 생각