최근 React 프로젝트에서 체크박스 컴포넌트를 개발하던 중, 체크 표시의 색상을 동적으로 변경하고 싶었습니다. 그래서 다음과 같이 코드를 작성했습니다:
<div className={`text-${checkColor} opacity-0 peer-checked:opacity-100`}>
<Check className="h-3 w-3" />
</div>
하지만 페이지를 확인해보니 색상이 전혀 적용되지 않았습니다. 왜 그럴까요?
알고 보니 Tailwind CSS의 핵심 작동 방식과 관련된 문제였습니다.
간단히 말해, text-red-500
처럼 직접 작성한 클래스는 포함되지만, text-${변수}
형태로 동적 생성된 클래스는 CSS에 포함되지 않기 때문에 스타일이 적용되지 않는 것입니다.
흥미롭게도, 다음과 같은 코드는 잘 작동합니다:
<div
className={`h-4 w-4 rounded ${
variant === 'default'
? 'peer-checked:border-primary-50 peer-checked:bg-primary-50 border border-gray-300 bg-white'
: 'border-transparent bg-transparent'
}`}
></div>
이는 많은 개발자들을 혼란스럽게 합니다. "왜 어떤 동적 코드는 되고, 어떤 것은 안될까?"
동작하지 않는 예: 클래스명 자체가 동적으로 생성되는 경우
className={`text-${checkColor}`}
checkColor
가 'red-500'이면 'text-red-500'이 되어야 하지만, 빌드 시점에는 알 수 없음동작하는 예: 정적 클래스명 중 조건부로 선택하는 경우
className={isRed ? 'text-red-500' : 'text-blue-500'}
핵심 차이점: 동적으로 새로운 클래스명을 생성하는 것은 안 되지만, 정적으로 정의된 클래스명들 중에서 선택하는 것은 가능합니다.
이 문제에 대해 팀원이 다음과 같은 코드 리뷰를 남겼습니다:
"동적으로 지정하신 클래스 명은 코드래빗 리뷰처럼 빌드시 안먹힐 수도 있어서, clsx로 처리해보심은 어떨까요?"
clsx는 CSS 클래스를 조건부로 결합하는 데 사용되는 유틸리티 라이브러리입니다. 복잡한 조건부 클래스 처리를 더 깔끔하게 할 수 있게 해줍니다.
import clsx from 'clsx';
// 조건부 클래스 적용
<div className={clsx(
'base-class',
isActive && 'active-class',
error ? 'error-class' : 'normal-class'
)}>
Hello
</div>
하지만 중요한 점은 clsx도 Tailwind의 동적 클래스 생성 문제를 해결해주지 않는다는 것입니다:
// 이렇게 해도 여전히 문제가 있음
<div className={clsx(`text-${checkColor}`, 'opacity-0')}>
Hello
</div>
clsx는 클래스 결합을 깔끔하게 관리하는 도구일 뿐, 빌드 타임에 인식되지 않는 동적 클래스 문제 자체는 해결해주지 않습니다.
이 문제를 해결할 수 있는 네 가지 방법이 있습니다:
<Check
className="h-3 w-3"
style={{ color: checkColor }} // 직접 색상 지정
/>
사용할 모든 가능한 클래스를 미리 등록하는 방법입니다.
// tailwind.config.js
module.exports = {
// 기존 설정...
safelist: [
'text-white',
'text-red-500',
'text-blue-500',
// 사용할 가능성 있는 모든 클래스들
]
}
<div
className="text-[var(--check-color)]"
style={{ '--check-color': checkColor }}
>
<Check className="h-3 w-3" />
</div>
모든 가능한 색상을 조건부로 적용합니다:
<div className={clsx({
'text-red-500': checkColor === 'red-500',
'text-blue-500': checkColor === 'blue-500',
'text-green-500': checkColor === 'green-500',
'opacity-0': true,
'peer-checked:opacity-100': true,
})}>
<Check className="h-3 w-3" />
</div>
이 방법은 가능한 모든 케이스를 미리 알고 있을 때만 사용 가능합니다.
우리 프로젝트에서는 체크박스 컴포넌트를 다음과 같이 수정했습니다:
// 수정 전 (작동하지 않음)
<div className={`text-${checkColor} opacity-0 peer-checked:opacity-100`}>
<Check className="h-3 w-3" />
</div>
// 수정 후 (제대로 작동함)
<div className="opacity-0 peer-checked:opacity-100">
<Check
className="h-3 w-3"
style={{ color: checkColor }}
/>
</div>
text-${checkColor}
는 작동하지 않는데, 조건부 클래스 선택은 작동하나요?// 작동하지 않음
className={`text-${checkColor}`}
// 작동함
className={`${variant === 'default' ? 'text-white' : 'text-gray-300'}`}
A: 이는 Tailwind의 빌드 프로세스 때문입니다:
첫 번째 예제에서는 빌드 시점에 text-${checkColor}
라는 텍스트만 보입니다. Tailwind는 checkColor
에 어떤 값이 들어갈지 모르기 때문에 필요한 CSS를 생성할 수 없습니다.
두 번째 예제에서는 빌드 시점에 문자열 'text-white'
와 'text-gray-300'
이 모두 인식됩니다. 실제 어떤 값이 선택될지는 런타임에 결정되지만, 두 클래스 모두 빌드 시점에 인식되어 CSS에 포함됩니다.
간단한 규칙: 클래스 이름 자체가 완전한 문자열로 코드에 있어야 합니다. 변수나 표현식으로 클래스 이름의 일부를 만들면 작동하지 않습니다.
Tailwind CSS와 함께 작업할 때 알아두면 좋은 내용들:
text-${dynamicValue}
피하기)Tailwind CSS는 강력한 도구이지만, 동적 클래스 생성에는 주의해야 합니다. 팀원의 코드 리뷰에서 언급된 clsx는 코드를 더 깔끔하게 만들어주는 좋은 도구이지만, Tailwind의 동적 클래스 문제 자체를 해결해주지는 않습니다.
이 문제를 해결하려면 인라인 스타일, CSS 변수, safelist, 또는 미리 정의된 클래스들을 조건부로 적용하는 방법 중 하나를 선택해야 합니다. 각 프로젝트의 상황과 필요에 맞는 방법을 선택하면 됩니다.
Tailwind CSS와 React를 함께 사용할 때 이러한 특성을 이해하면, 스타일링 오류를 예방하고 더 효율적으로 개발할 수 있습니다!