1. 사건 개요 - 파란 버튼이 파랗지 않다
신규 프로젝트 마감 전날, 저는 기이한 현상을 목격했습니다.
분명히 버튼에 bg-blue-500을 줬는데, 버튼은 여전히 투명했습니다.
<button className={`bg-${color}-500 text-white`}>
Click Me
</button>
개발자 도구(F12)를 켜서 확인해 보니, button 태그에 클래스는 정확히 들어있었습니다.
하지만 Computed 탭이나 Styles 탭을 아무리 뒤져봐도 background-color 속성은 없었죠.
마치 유령처럼, 이름만 있고 실체는 없는 클래스였습니다.
이 수사 기록은 그날 밤 제가 범인(버그)을 잡기 위해 파헤친 5가지 용의자 리스트입니다.
용의자 1 - 동적 클래스 생성 (가장 유력한 범인)
Tailwind는 런타임에 스타일을 만드는 게 아니라, 빌드 타임에 파일을 스캔해서 스타일을 만듭니다.
즉, PurgeCSS(JIT 컴파일러)가 소스 코드를 텍스트로 읽어서 "어, bg-blue-500이라는 문자열이 있네?" 하면 CSS를 생성합니다.
문제의 코드를 다시 봅시다.
// ❌ 범인은 바로 이 녀석
const color = 'blue';
<button className={`bg-${color}-500`}>Click</button>
컴파일러는 코드를 실행해보지 않습니다. 그냥 텍스트로만 봅니다.
컴파일러 눈에는 bg-와 -500은 보이지만, bg-blue-500이라는 완성된 문자열은 보이지 않습니다.
그래서 "아, 이 클래스는 안 쓰는구나" 하고 CSS 파일에서 제외시켜 버립니다.
해결책: 완성된 문자열을 쓰세요.
// ✅ 전체 문자열을 매핑해서 사용
const colorVariants = {
blue: 'bg-blue-500',
red: 'bg-red-500',
green: 'bg-green-500'
};
<button className={colorVariants[color]}>Click</button>
이제 컴파일러는 bg-blue-500을 명확하게 볼 수 있습니다.
용의자 2 - content 설정 누락 (공범)
tailwind.config.js 파일의 content 배열은 컴파일러의 "수색 범위"입니다.
여기에 파일 경로가 빠져 있다면, Tailwind는 그 파일을 쳐다보지도 않습니다.
// tailwind.config.js
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
// ⚠️ 혹시 새로운 폴더(components/ui 등)를 만들고 여기 추가 안 했나요?
],
// ...
}
제 경우, 모노레포 구조에서 공유 UI 패키지의 경로를 content에 추가하지 않아서 발생한 적이 있습니다.
새로운 디렉토리를 만들었다면 꼭 수색 영장(content)에 추가해주세요.
용의자 3 - CSS 우선순위 (Specificity) 전쟁
클래스는 적용됐는데 다른 스타일에 덮어씌워진 경우입니다. 특히 기존 CSS 라이브러리(Bootstrap 등)와 함께 쓸 때 자주 발생합니다.
개발자 도구에서 스타일이 취소선(strike-through)으로 그어져 있다면 이 경우입니다.
해결책 1: !important 사용 (비추천)
<div className="bg-red-500!" /> {/* Tailwind 3.x 문법 */}
해결책 2: Tailwind 우선순위 높이기
CSS 파일에서 @tailwind base; 등이 다른 스타일보다 아래에 오도록 하거나, ID 선택자 등으로 감싸서 점수를 높이세요.
용의자 4 - 캐시 (JIT 컴파일러의 게으름)
가끔은 그냥 컴퓨터가 멍청해서 발생합니다. 설정을 바꿨는데도 반영이 안 된다면? JIT 컴파일러가 이전 결과를 캐시하고 있을 수 있습니다.
해결책: 껐다 켜기 & 캐시 삭제
- 개발 서버 재시작 (
npm run dev다시 실행) .next폴더나node_modules/.cache폴더 삭제
"껐다 켜니까 되는데요?"는 개발계의 영원한 진리입니다.
용의자 4-1 - "무조건 있어야 하는 클래스" (SafeList)
만약 동적 클래스를 꼭 써야 한다면? (예: CMS에서 색상을 받아오는 경우)
safelist 옵션을 사용하면 컴파일러에게 "이건 무조건 남겨둬"라고 명령할 수 있습니다.
// tailwind.config.js
module.exports = {
safelist: [
'bg-red-500',
'text-3xl',
{
pattern: /bg-(red|green|blue)-(400|500)/, // 정규식도 가능!
},
],
// ...
}
하지만 이 방법은 CSS 파일 크기를 키우므로 꼭 필요한 경우에만 사용하세요.
용의자 5 - 특수 문자 이스케이프
클래스 이름에 /, [, ] 같은 특수문자가 들어갈 때 발생합니다.
예를 들어 w-1/2는 괜찮지만, 커스텀 값 w-[50%] 같은 걸 쓸 때 가끔 문제가 됩니다.
HTML 클래스 속성에서는 괜찮지만, document.querySelector 같은 걸로 찾을 때는 이스케이프가 필요합니다.
하지만 React 컴포넌트 내부라면 보통은 문제가 안 됩니다. 다만, 오타가 없는지 확인하세요. 의외로 text-blud-500 같은 오타가 범인일 때가 많습니다.
수사 종결 - 범인은 내 안에 있었다
제 사건의 범인은 역시 용의자 1번(동적 클래스 문자열 조합)이었습니다.
bg-${props.color}-500을 clsx 라이브러리와 객체 매핑으로 바꾸자마자 파란색 버튼이 영롱하게 빛났습니다.
여러분도 스타일이 실종됐다면, 당황하지 말고 이 5가지 용의자를 차례로 심문해 보세요. 범인은 반드시 이 안에 있습니다.
번외 - 실제 운영 환경 배포 사고 (Case Study)
제가 겪었던 황당한 사고입니다. 로컬에서는 잘 나오는데, 프로덕션 배포만 하면 스타일이 깨지는 겁니다.
원인: CI/CD 파이프라인에서 NODE_ENV=production으로 빌드할 때, Tailwind가 불필요한 클래스를 너무 공격적으로 제거(Purge)했습니다.
이유는, 제가 백엔드에서 API로 받아온 데이터를 클래스 이름으로 쓰고 있었기 때문입니다.
status가 active면 text-green-500, inactive면 text-gray-500.
이게 DB에만 있고 코드에는 없으니까, 배포 빌드 때 싹 날아간 거죠.
교훈: DB 데이터로 스타일링 할 때는 반드시 SafeList를 쓰거나, 프론트엔드 코드 내에 map 객체를 만들어야 합니다.