이 글에서 정리하는 내용
React에서 Tailwind CSS를 사용할 때 처럼 조립한 클래스명이 왜 적용되지 않는지 정리합니다. Tailwind CSS v4 기준으로 Tailwind는 JavaScript 실행 결과가 아니라 소스 파일 안의 완성된 클래스 문자열을 기준으로 CSS를 생성합니다. 그래서 동적 값은 key로 받고, Tailwind 클래스는 완성된 문자열로 남겨두는 구조가 필요합니다.
- React에서는 자연스러운데 Tailwind에서는 안 되는 코드
- Tailwind는 JavaScript 실행 결과를 보지 않는다
- colorMap으로 바꾸면 왜 해결되는가
- 실무에서는 color보다 variant로 관리하는 쪽이 낫다
- clsx, @source, safelist를 헷갈리지 않기
- 다음에 다시 확인할 기준
React에서는 자연스러운데 Tailwind에서는 안 되는 코드

React에서 Tailwind를 쓰다 보면 상태나 props에 따라 색상을 바꾸고 싶을 때가 많습니다. 관리자 화면의 상태 뱃지, 상품 카드의 카테고리 라벨, 버튼의 variant처럼 작은 UI일수록 이런 코드가 자연스럽게 나옵니다.
예를 들어 라는 props를 받아서 배경색을 바꾸는 Badge 컴포넌트를 만든다고 하면, 처음에는 아래처럼 작성하기 쉽습니다.
function Badge({ color }) { return ( <span className={`bg-${color}-500 text-white px-2 py-1 rounded`}> Badge </span> );
} React 문법만 놓고 보면 이상한 코드는 아닙니다. 가 라면 최종 은 가 될 것처럼 보입니다. 를 넘기면을 넘기면 이 될 것처럼 보입니다.
문제는 브라우저에서 최종 문자열이 만들어지는 것과 Tailwind가 CSS를 생성하는 시점이 다르다는 점입니다. Tailwind는 사용자가 컴포넌트를 클릭하거나 props가 들어온 뒤의 결과를 기다렸다가 CSS를 만드는 도구가 아닙니다. 빌드 과정에서 프로젝트 파일을 훑고, 그 안에 있는 클래스 후보를 기준으로 필요한 CSS를 생성합니다.
그래서 이 문제를 React의 조건부 렌더링 문제로 보면 해결이 늦어집니다. 실제 원인은 문자열 조립 방식과 Tailwind의 클래스 감지 방식이 맞지 않는 데 있습니다. Tailwind의 기본 클래스 흐름이 아직 헷갈린다면 Tailwind CSS 기본 클래스: text font bg spacing 사용 기준처럼가 어떤 역할을 하는지 먼저 확인하면 이후의 동적 클래스 문제도 원인 분리가 됩니다.
Tailwind는 JavaScript 실행 결과를 보지 않는다
Tailwind가 보는 것은 “실행된 React 컴포넌트”가 아니라 “소스 파일 안에 적힌 문자열”입니다. 이 차이를 놓치면 은 DOM에 들어갔는데 CSS 규칙은 생성되지 않는 상황을 React 문제로 오해하기 쉽습니다.
아래 코드에는 Tailwind가 바로 찾을 수 있는 완성된 클래스명이 없습니다.
className={`bg-${color}-500 text-white`} 파일 안에 실제로 존재하는 조각은 입니다. 사람이 보기에는 세 조각이 합쳐져 이 될 수 있다고 예상하지만, Tailwind는 이 JavaScript 표현식을 실행해서 가능한 모든 결과를 계산하지 않습니다. 그래서같은 완성된 클래스명이 소스 안에 없으면 해당 CSS가 생성되지 않을 수 있습니다.
반대로 아래처럼 완성된 클래스명이 문자열로 남아 있으면 Tailwind가 감지할 수 있습니다.
const colorMap = { red: "bg-red-500 text-white", blue: "bg-blue-500 text-white", green: "bg-green-500 text-white"}; 여기서 중요한 부분은 객체를 썼다는 사실 자체가 아닙니다.처럼 Tailwind가 감지할 수 있는 완성된 클래스명이 코드 안에 그대로 존재한다는 점입니다.
간혹 “개발 서버에서는 되는 것 같은데 배포 후에 안 된다”는 식으로 느껴질 때도 있습니다. 실제로는 브라우저 DOM에 이 들어간 것과 CSS 파일에 해당 클래스 규칙이 생성된 것은 별개의 문제입니다. 개발자 도구에서 이 보이더라도, 최종 CSS에 규칙이 없다면 화면은 바뀌지 않습니다.
colorMap으로 바꾸면 왜 해결되는가
해결 방식은 클래스명을 런타임에 조립하지 않고가능한 스타일 조합을 미리 완성된 문자열로 남기는 것입니다. 사용자가 제공한 Badge 예시는 아래처럼 바꿀 수 있습니다.
const colorMap = { red: "bg-red-500 text-white", blue: "bg-blue-500 text-white", green: "bg-green-500 text-white"}; function Badge({ color = "blue" }) { return ( <span className={`${colorMap[color]} px-2 py-1 rounded text-sm font-medium`}> Badge </span> );
} 이 코드에서는 값 자체는 여전히 동적입니다. props로, 중 하나가 들어올 수 있습니다. 다만 Tailwind 클래스명은 더 이상 처럼 조립하지 않습니다. 이미 완성된 문자열 중 하나를 선택합니다.
이 방식은 Tailwind 감지 문제만 해결하지 않습니다. 허용할 색상 범위도 코드에서 분명해집니다.같은 값이 갑자기 들어와도 에 없으면 확인할 위치가 명확합니다. API 응답값이나 사용자 입력값을 그대로 클래스명에 끼워 넣는 구조보다 통제 범위가 좁아집니다.
단이대로만 쓰면 가 없는 경우 가 에 들어갈 수 있습니다. 작은 예제에서는 놓치기 쉽지만, 관리자 목록처럼 서버에서 내려온 상태값을 표시하는 화면에서는 예외값이 섞일 수 있습니다.
const colorMap = { red: "bg-red-500 text-white", blue: "bg-blue-500 text-white", green: "bg-green-500 text-white"}; function Badge({ color = "blue" }) { const colorClass = colorMap[color] ?? colorMap.blue; return ( <span className={`${colorClass} px-2 py-1 rounded text-sm font-medium`}> Badge </span> );
} 이렇게 fallback을 두면 예상하지 못한 값이 들어와도 최소한 기본 스타일이 유지됩니다. Tailwind 문제를 해결하면서 컴포넌트의 입력값 방어까지 같이 잡는 구조입니다.
실무에서는 color보다 variant로 관리하는 쪽이 낫다
처음에는 같은 색상 이름으로 관리해도 충분해 보입니다. 하지만 실제 UI에서는 색상보다 의미가 먼저인 경우가 많습니다. 예를 들어 주문 상태 뱃지라면 빨간색인지 초록색인지보다 “실패”, “완료”, “대기”가 더 중요한 기준입니다.
그래서 컴포넌트가 조금만 커지면 보다 를 받는 구조가 수정 기준을 더 분명하게 만듭니다.
const badgeVariants = { success: "bg-green-100 text-green-700 ring-1 ring-green-200", warning: "bg-yellow-100 text-yellow-800 ring-1 ring-yellow-200", error: "bg-red-100 text-red-700 ring-1 ring-red-200", default: "bg-gray-100 text-gray-700 ring-1 ring-gray-200"}; function StatusBadge({ variant = "default", children }) { const variantClass = badgeVariants[variant] ?? badgeVariants.default; return ( <span className={`${variantClass} inline-flex rounded-full px-2.5 py-1 text-xs font-medium`}> {children} </span> );
} 이 구조에서는 서버에서 같은 값이 오더라도 그 값을 바로 Tailwind 클래스명으로 쓰지 않습니다. 먼저 화면에서 사용할 의미로 변환한 뒤, 그 의미에 맞는 완성된 클래스 문자열을 선택합니다.
const orderStatusVariant = { paid: "success", pending: "warning", failed: "error"}; function OrderStatusBadge({ status }) { const variant = orderStatusVariant[status] ?? "default"; return <StatusBadge variant={variant}>{status}</StatusBadge>;
} 이렇게 나누면 API 상태값, UI 의미, Tailwind 클래스가 서로 엉키지 않습니다. 상태값이 바뀌면 를 보면 되고, 색상 디자인이 바뀌면 를 보면 됩니다. 클래스명을 직접 조립하는 방식보다 수정 위치가 명확합니다.
색상 기준이 점점 많아지는 프로젝트라면 Tailwind @theme 사용법: 변수와 디자인 토큰 차이와 연결해서 볼 수 있습니다. 단순히를 늘리는 것보다 브랜드 색상, 상태 색상, 강조 색상을 어떤 기준으로 나눌지 먼저 정해야 나중에 클래스 조합이 덜 흔들립니다.
또 하나 헷갈리는 지점은 입니다. 처럼 쓰면 해결될 것 같지만이 역시 런타임에 조립되는 문자열이라면 Tailwind가 감지하기 어렵습니다. 대괄호 문법 자체를 다뤄야 한다면 Tailwind Arbitrary Value Variant 차이: 대괄호 문법 사용 기준에서 임의 값과 임의 variant의 역할을 따로 확인해야 합니다.
clsx, safelist를 헷갈리지 않기

동적 클래스 문제가 나오면같은 이름도 같이 따라옵니다. 이 도구들은 전부 같은 문제를 해결하는 기능이 아닙니다. 역할을 섞어서 이해하면 오히려 코드가 더 복잡해집니다.
먼저 는 조건부로 클래스 문자열을 합치는 도구입니다. 하지만 Tailwind가 런타임에 조립된 클래스를 이해하게 만들어주지는 않습니다.
import clsx from "clsx"; function Badge({ active }) { return ( <span className={clsx( "px-2 py-1 rounded text-sm", active ? "bg-blue-500 text-white" : "bg-gray-100 text-gray-700" )} > Badge </span> );
} 이 코드는 괜찮습니다.이 모두 완성된 문자열로 코드 안에 존재하기 때문입니다. 를 써서 되는 것이 아니라, Tailwind가 감지할 수 있는 클래스명이 남아 있어서 되는 구조입니다.
import clsx from "clsx"; function Badge({ color }) { return ( <span className={clsx(`bg-${color}-500`, "text-white px-2 py-1")}> Badge </span> );
} 반대로 이 코드는 를 써도 본질적인 문제는 그대로입니다. 같은 완성된 클래스명이 코드 안에 없기 때문입니다.
는 다른 성격입니다. Tailwind가 기본적으로 스캔하지 않는 외부 경로나 패키지 안의 파일을 명시적으로 등록할 때 쓰는 기능입니다. 예를 들어 별도 UI 패키지 안에 Tailwind 클래스가 있는데, 프로젝트의 기본 스캔 범위에 들어오지 않는다면 로 추가할 수 있습니다.
@import "tailwindcss";
@source "../node_modules/@acmecorp/ui-lib"; 하지만 를 추가한다고 해서 처럼 조립한 문자열을 Tailwind가 자동으로 해석하는 것은 아닙니다. 스캔할 위치를 알려주는 것과 런타임 문자열 조립을 분석하는 것은 다른 문제입니다.
Tailwind CSS v4에서는 특정 유틸리티를 강제로 생성해야 할 때 을 사용할 수 있습니다. 다만 이것도 기본 해결책으로 먼저 꺼낼 성격은 아닙니다. 특정 클래스가 소스 안에 직접 나오지 않지만 반드시 생성되어야 하는 상황에서 제한적으로 쓰는 장치에 가깝습니다. 대부분의 React 컴포넌트에서는 먼저 map 객체나 variant 객체로 해결하는 쪽이 코드 추적에 유리합니다.
반복되는 버튼이나 뱃지 스타일을 어디까지 컴포넌트에서 관리하고 어디서 CSS 유틸리티로 분리할지 고민된다면 Tailwind @apply 기준: @utility custom variant 차이와 연결해서 판단하면 됩니다. 다만 이 글의 문제는 우선 “클래스가 생성되지 않는 원인”이므로, 나 커스텀 유틸리티로 너무 빨리 넘어가면 원인 파악이 흐려집니다.
다음에 다시 확인할 기준
Tailwind 동적 클래스 문제는 코드가 길어서 생기는 문제가 아닙니다. 짧은 Badge 컴포넌트에서도 바로 발생할 수 있습니다. 기준은 단순합니다. Tailwind가 파일을 스캔할 때 완성된 클래스명을 찾을 수 있어야 합니다.
처럼 조립하는 방식은 React 입장에서는 자연스럽지만, Tailwind 입장에서는 이라는 문자열이 존재하지 않는 코드입니다. 그래서 동적 값은 조각으로 직접 끼워 넣지 말고, map 객체의 key로 사용하는 쪽이 안정적입니다.
컴포넌트를 다시 볼 때는 아래 순서로 확인하면 됩니다.
| 확인할 부분 | 판단 기준 |
|---|---|
| 클래스명이 조립되고 있는가 | 처럼 문자열 일부를 props로 만들고 있다면 수정 대상입니다. |
| 완성된 클래스명이 코드에 있는가 | 처럼 전체 클래스명이 문자열로 남아 있어야 합니다. |
| 외부 값이 바로 에 들어가는가 | API 값은 바로 클래스명으로 쓰지 말고 variant key로 변환합니다. |
| clsx로 해결하려는가 | clsx는 조합 도구일 뿐, 동적 클래스 감지 문제를 해결하지 않습니다. |
| 가 필요한 문제인가 | 스캔 경로 문제인지, 런타임 조립 문제인지 먼저 나눠야 합니다. |
| inline()이 필요한가 | 소스에 없는 유틸리티를 강제로 생성해야 하는 예외 상황인지 확인합니다. |
상태 스타일링이 더 복잡해져서 hover, disabled, aria, data 속성까지 함께 다뤄야 한다면 단순 색상 매핑만으로는 부족할 수 있습니다. 그런 경우에는 상태 기준을 먼저 나누고, 필요하면 Tailwind CSS group peer has 차이: 상태 기준으로 고르는 방법처럼 상태가 발생하는 요소와 실제로 스타일이 바뀌는 요소의 관계까지 같이 봐야 합니다.
마지막에 남길 기준은 하나입니다. React에서 동적인 값은 필요하지만, Tailwind 클래스명은 가능한 한 정적으로 남겨야 합니다. 색상, 크기, 상태가 늘어날수록 문자열을 조립하는 방식보다 의미 있는 key와 완성된 클래스 문자열을 연결하는 방식이 수정 지점을 더 선명하게 남깁니다.