Tailwind CSS

Tailwind CSS 동적 클래스가 적용되지 않는 이유와 @source 사용 기준

2026.05.02·수정 2026.05.12·약 14분

이 글에서 정리하는 내용

Tailwind CSS v4 기준으로 동적 클래스가 적용되지 않는 이유를 클래스 감지 방식에서부터 정리합니다. 처럼 런타임에 조합되는 문자열이 왜 실패하는지, 먼저 어떤 방식으로 고쳐야 하는지, 그리고 와 을 어떤 상황에서 써야 하는지까지 나눠봅니다.

동적 클래스가 안 먹는 순간은 어디서 시작될까

Tailwind CSS에서 동적 클래스가 감지되지 않는 흐름을 보여주는 구조도

Tailwind CSS에서 동적 클래스 문제는 화면만 보면 원인을 바로 찾기 어렵습니다. React 개발자 도구에는 같은 문자열이 들어간 것처럼 보이는데, 실제 화면에는 배경색이 적용되지 않는 경우가 있습니다. 이때 처음에는 조건문, CSS 우선순위, 캐시, 빌드 오류를 먼저 의심하기 쉽습니다.

하지만 Tailwind에서 이 문제가 반복된다면 확인 순서를 바꿔야 합니다. 브라우저에 최종적으로 어떤 이 들어갔는지보다, Tailwind가 빌드 과정에서 해당 클래스명을 완성된 문자열로 발견할 수 있었는지가 먼저입니다. 화면에 문자열이 생기는 시점과 Tailwind가 CSS를 만드는 시점이 다르기 때문입니다.

실패하기 쉬운 코드

const color = "red"; export default function Badge() { return <span className={`bg-${color}-500 text-white px-3 py-1 rounded-full`}>진행중</span>;
}

이 코드는 React 문법만 보면 크게 이상하지 않습니다. 값이 라면 런타임에서 최종 문자열은 처럼 만들어집니다. 문제는 Tailwind가 이 코드를 실행해서 결과 문자열을 계산하지 않는다는 데 있습니다. Tailwind 입장에서는 이라는 조각만 보일 뿐이고, 소스 파일 안에 이라는 완성된 클래스명은 없습니다.

그래서 동적 클래스 문제를 볼 때는 “문자열이 만들어지는가”와 “Tailwind가 그 문자열을 미리 볼 수 있는가”를 분리해야 합니다. 전자는 React나 JavaScript의 영역이고, 후자는 Tailwind의 클래스 감지 영역입니다.

Tailwind는 JavaScript를 실행하지 않고 스캔한다

Tailwind CSS는 프로젝트 파일을 스캔해서 사용된 유틸리티 클래스를 찾고, 발견한 클래스에 해당하는 CSS를 생성합니다. 이 과정은 JavaScript를 실행하는 방식이 아니라 소스 파일을 일반 텍스트처럼 훑는 방식에 가깝습니다. 그래서 코드가 런타임에 어떤 문자열을 만들어낼 수 있는지는 기본 감지 대상이 아닙니다.

이 기준을 알고 나면 문제가 조금 단순해집니다. Tailwind가 볼 수 있는 형태로같은 완성된 클래스명이 남아 있으면 CSS가 생성됩니다. 반대로 템플릿 리터럴, 문자열 더하기, 서버 데이터 조합처럼 클래스명이 조각난 상태로만 존재하면 해당 CSS는 만들어지지 않을 수 있습니다.

조건문은 괜찮고, 조각난 문자열이 문제다

export default function Badge({ isActive }: { isActive: boolean }) { return ( <span className={isActive ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-700"}> 상태 </span> );
}

여기서도 조건에 따라 클래스가 바뀝니다. 다만 각 클래스명은 소스 안에 완성된 문자열로 들어 있습니다. Tailwind는 을 그대로 발견할 수 있습니다. 동적 조건 자체가 문제가 아니라, Tailwind가 읽을 수 없는 방식으로 클래스명을 조립했는지가 문제입니다.

이 차이를 놓치면 를 너무 빨리 찾게 됩니다. 그런데 대부분의 버튼 variant, badge 색상, 카드 강조 상태는 보다 코드 구조를 바꾸는 쪽이 먼저입니다. 가능한 스타일 후보를 코드 안에 완성된 클래스명으로 남겨두면 Tailwind와 작업자 모두 같은 기준을 보게 됩니다.

가장 먼저 선택할 해결책은 클래스 매핑이다

상태값에 따라 색상이나 크기를 바꾸고 싶다면 클래스 매핑을 먼저 고려하는 것이 실무에서 다루기 쉽습니다. 관리자 목록, 상품 상태 badge, 버튼 variant처럼 가능한 경우의 수가 정해져 있는 UI는 매핑 객체가 더 명확합니다.

예를 들어 주문 상태가로 나뉘는 badge를 만든다고 가정해보겠습니다. 서버에서 내려온 상태값을 그대로 클래스 조합에 끼워 넣으면 색상 기준이 코드 곳곳으로 흩어집니다. 반대로 상태별 클래스를 한곳에 모아두면 Tailwind도 감지할 수 있고, 디자인 변경 시 수정 위치도 분명해집니다.

상태값을 완성된 클래스명으로 연결하기

const badgeClassByStatus = { pending: "bg-yellow-100 text-yellow-800 border-yellow-200", paid: "bg-green-100 text-green-800 border-green-200", failed: "bg-red-100 text-red-800 border-red-200"} as const; type OrderStatus = keyof typeof badgeClassByStatus; export default function OrderBadge({ status }: { status: OrderStatus }) { return ( <span className={`inline-flex rounded-full border px-3 py-1 text-sm ${badgeClassByStatus[status]}`}> {status} </span> );
}

이 방식은 Tailwind 감지 문제만 해결하지 않습니다. 상태와 스타일의 관계가 한곳에 남기 때문에 작업자가 바뀌어도 기준을 찾기 쉽습니다. 예를 들어 상태의 초록색을 브랜드 컬러로 바꿔야 한다면 JSX 전체를 뒤질 필요 없이 만 보면 됩니다.

또 하나의 장점은 허용되는 상태값이 타입으로 드러난다는 점입니다. TypeScript를 함께 쓰면 같은 새 상태가 추가되었을 때 매핑이 빠졌는지 확인하기 쉽습니다. Tailwind 문제에서 출발했지만, 결과적으로 UI 상태 설계까지 정리되는 구조입니다.

버튼 variant도 같은 기준으로 볼 수 있다

const buttonVariants = { primary: "bg-slate-900 text-white hover:bg-slate-700", secondary: "bg-white text-slate-900 border border-slate-300 hover:bg-slate-50", danger: "bg-red-600 text-white hover:bg-red-500"} as const; export function Button({ variant = "primary", children }: { variant?: keyof typeof buttonVariants; children: React.ReactNode;
}) { return ( <button className={`rounded-lg px-4 py-2 text-sm font-medium ${buttonVariants[variant]}`}> {children} </button> );
}

문자열 조합은 처음에는 짧아 보입니다. 하지만 variant가 늘어나면 조건이 복잡해지고, 어떤 값이 허용되는지도 흐려집니다. Tailwind에서는 가능한 클래스를 미리 드러내는 코드가 CSS 생성에도 유리하고 유지보수에도 유리합니다.

해결책을 고르는 기준

상황 먼저 볼 해결책 이유
상태 badge, 버튼 variant처럼 경우의 수가 정해져 있음 클래스 매핑 완성된 클래스가 코드에 남고 수정 위치가 명확함
클래스는 완성되어 있지만 Tailwind가 해당 파일을 스캔하지 않음 자동 감지 범위 밖의 파일을 스캔 대상으로 추가해야 함
실제 파일에는 없지만 특정 유틸리티 CSS가 반드시 필요함 필요한 클래스만 명시적으로 생성해야 함

@source는 언제 필요한가

Tailwind CSS v4에서는 CSS 파일 안에서 를 사용해 Tailwind가 스캔할 소스 경로를 명시할 수 있습니다. 이 문법은 동적 문자열을 해석해주는 기능이 아닙니다. 자동 감지에서 빠지는 파일이나 경로를 Tailwind의 스캔 대상으로 추가하는 역할에 가깝습니다.

예를 들어 모노레포에서 공통 UI 패키지를 따로 두고 있거나, 안의 Tailwind 기반 컴포넌트 라이브러리를 사용하거나, 기본 스캔 범위 밖에 관리자 화면이 있는 경우가 있습니다. 해당 파일 안에는 완성된 Tailwind 클래스가 있는데도 Tailwind가 그 파일을 보지 못하면 CSS가 빠질 수 있습니다. 이런 상황에서 를 사용합니다.

외부 UI 패키지를 스캔 대상으로 추가하는 예시

@import "tailwindcss"; @source "../node_modules/@acme/ui-components";

이 코드는 패키지 안에 들어 있는 Tailwind 클래스까지 스캔하도록 알려주는 예시입니다. 단이 경로 안에도 클래스명이 완성된 문자열로 존재해야 합니다. 를 추가했다고 해서 같은 조합식이 자동으로으로 확장되는 것은 아닙니다.

자동 감지를 끄고 필요한 경로만 지정하는 예시

@import "tailwindcss" source(none); @source "../app";
@source "../components";

프로젝트 구조가 크거나 빌드 대상이 분리되어 있다면 자동 감지 범위를 줄이고 필요한 경로만 지정할 수도 있습니다. 다만 이 방식은 빠뜨린 경로가 있으면 스타일이 통째로 누락될 수 있습니다. 작은 프로젝트에서는 자동 감지를 유지하고, 실제로 빠지는 경로만 추가하는 방식이 더 단순합니다.

제외해야 하는 경로가 있는 경우

@import "tailwindcss"; @source not "../src/legacy";

오래된 레거시 화면이나 테스트용 샘플 파일 때문에 불필요한 유틸리티가 많이 생성되는 경우에는 제외 설정을 둘 수 있습니다. 다만 처음부터 제외 범위를 넓게 잡으면 필요한 클래스까지 빠질 수 있습니다. 적용 후에는 빌드된 화면에서 주요 페이지의 상태 스타일, hover 스타일, 반응형 클래스가 빠지지 않았는지 확인해야 합니다.

inline()으로 강제 생성해야 하는 경우

Tailwind CSS @source와 @source inline 사용 기준을 비교한 체크리스트 UI

은 실제 소스 파일에 클래스명이 없더라도 특정 유틸리티를 생성하도록 지정할 때 사용합니다. Tailwind v3의 를 떠올리면 이해하기 쉽지만, v4에서는 CSS 안에서 선언한다는 점이 다릅니다.

예를 들어 CMS에서 색상 이름을 고르게 만들었고, 실제 화면에서는 선택된 값에 따라, 중 하나가 들어간다고 가정해보겠습니다. 이 클래스들이 JSX나 HTML 안에 완성된 문자열로 남아 있지 않다면 Tailwind는 CSS를 생성하지 않을 수 있습니다. 이럴 때 제한된 범위만 으로 남길 수 있습니다.

필요한 유틸리티만 직접 생성하는 예시

@import "tailwindcss"; @source inline("bg-red-500 bg-blue-500 bg-green-500 text-white");

이 방식은 클래스 매핑으로 해결하기 어려운 경우에만 좁게 쓰는 것이 좋습니다. 실제로 가능한 색상 값이 정해져 있다면 React 코드 안에 매핑 객체를 두는 방식이 더 추적하기 쉽습니다. 은 코드만 봐서는 사용처가 바로 드러나지 않는 CSS를 만들기 때문에, 범위가 커질수록 관리 비용이 늘어납니다.

variant와 범위를 함께 생성하는 예시

@import "tailwindcss"; @source inline("{hover:focus:}bg-red-{100..900..100}");

여러 shade나 variant를 한 번에 생성해야 할 때는 이런 형태를 사용할 수 있습니다. 다만 편하다는 이유로 모든 색상과 모든 상태를 한꺼번에 열어두면 Tailwind를 쓰는 장점이 줄어듭니다. 필요한 UI가 badge 세 가지라면 세 가지를 직접 쓰는 편이 낫고, 관리자에서 색상 팔레트를 실제로 선택해야 한다면 허용 범위를 문서처럼 남겨두어야 합니다.

특히 은 문제를 숨기기 쉽습니다. 동적 클래스가 적용되지 않는다고 해서 모든 후보를 강제로 생성해버리면 당장은 화면이 맞을 수 있습니다. 하지만 시간이 지나면 실제로 쓰지 않는 유틸리티까지 CSS에 남고, 어떤 화면이 그 클래스를 요구하는지 추적하기 어려워집니다. 그래서 먼저 매핑으로 해결 가능한지 보고, 그 다음에 스캔 경로 문제인지, 마지막으로 강제 생성이 필요한지 순서대로 확인하는 것이 좋습니다.

다시 같은 문제를 만나지 않기 위한 체크포인트

Tailwind 동적 클래스 문제를 다시 만나면 의 최종 결과만 보지 말고, 소스 안에 완성된 클래스명이 존재하는지 확인해야 합니다.처럼 조각난 형태가 있다면 Tailwind가 CSS를 생성하지 못할 가능성이 큽니다.

가능한 값이 정해져 있다면 클래스 매핑으로 해결합니다. 상태 badge, 버튼 variant, 카드 강조 색상처럼 화면 규칙이 정해진 경우에는 이 방식이 가장 안정적입니다. 자동 스캔에서 빠지는 파일이 있다면 로 경로를 추가합니다. 실제 파일에는 없지만 반드시 생성해야 하는 유틸리티가 있다면 을 좁은 범위로 사용합니다.

확인 순서를 남겨두면 같은 문제를 덜 헤맵니다. 먼저 완성된 클래스명이 코드에 있는지 봅니다. 그다음 그 코드가 Tailwind의 스캔 대상인지 확인합니다. 마지막으로 클래스가 실제 파일에 남을 수 없는 구조인지 판단합니다. 이 세 단계를 지나도 필요할 때만 을 쓰면 됩니다.

Tailwind는 런타임에 만들어질 수 있는 모든 경우를 추측하지 않습니다. 소스에서 발견 가능한 클래스, 또는 명시적으로 생성하라고 알려준 클래스만 CSS로 남깁니다. 그래서 동적 클래스 문제는 단순한 문법 문제가 아니라, 스타일 후보를 어디에 어떤 형태로 남길지 정하는 구조 문제로 보는 것이 더 정확합니다.

같이 읽으면 좋은 글

이 글이 마음에 드세요?

RSS 피드를 구독하세요!

댓글 남기기