tailwind-merge를 사용하는 이유

tailwind-merge는 Tailwind CSS 환경에서 유용하게 쓰이는 라이브러리입니다.
프로젝트 내에서 공통 컴포넌트를 만들어 사용하다보면 특정 페이지 내에서 사용하는 컴포넌트에 추가적인 스타일을 적용해야 하는 경우가 발생하곤 합니다. 더욱 유연하게 컴포넌트 스타일을 조정하기 위해 클래스를 정의하여 추가하거나, 인라인 스타일을 사용하거나, CSS-in-JS의 경우 동적 스타일링을 활용할 수 있겠죠.
프로젝트에서 Tailwind CSS를 사용한다면 다음과 같이 코드를 작성할 수 있을 것입니다.
import { ComponentPropsWithoutRef } from 'react'
interface ButtonProps extends ComponentPropsWithoutRef<'button'> {}
const Button = ({ children, className }: ButtonProps) => (
<button
className=`text-white rounded bg-primary px-1 py-2 ${className || ''}`
>
{children}
</button>
)
export default Button
추가적인 스타일을 적용하고 싶다면 Button
컴포넌트를 다음과 같이 사용합니다.
<Button className='text-black p-3'>버튼</Button>
Button
컴포넌트의 기본 스타일에 더해 텍스트 색상이 검은색으로, padding
이 0.75rem
으로 적용되기를 기대하며 작성한 코드지만, 실제로는 아래처럼 text-black
, p-3
이 적용되지 않는 것을 볼 수 있습니다.

이는 CSS Cascade가 이루어질 때 Tailwind에서 정의한 클래스 순서에 따라 먼저 정의된 클래스가 무시되면서 발생하는 문제입니다.
이를 해결하기 위해 tailwind-merge에서 제공하는 twMerge
를 사용합니다.
<!-- Button 컴포넌트 -->
<button
className={twMerge('text-white rounded bg-primary px-1 py-2', className)}
>
{children}
</button>

중복되는 속성의 클래스는 제거되고, 가장 뒤에 오는 클래스가 선택되어 병합된 것을 볼 수 있습니다.
커스텀 클래스 병합 문제
문제 상황
twMerge
를 잘 사용하던 중, 특정한 경우에 병합이 제대로 이루어지지 않는 문제를 겪었습니다.
이 경우는 다음과 같았습니다.
<Button className='text-caption'>Hello</Button>
text-caption
은 프로젝트 내에서 사용 중인 커스텀 클래스입니다.
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{html,js,ts,jsx,tsx}'],
theme: {
extend: {
fontSize: {
caption: '11px',
},
// ...
}
},
}


크롬 개발자 도구를 통해 살펴보면 버튼 컴포넌트 스타일로 정의된 text-white
가 제거된 것을 볼 수 있습니다. 같은 prefix를 가진 text-caption
과 text-white
가 충돌했고, 병합 과정에서 뒤에 오는 text-caption
이 채택된 것이죠.
해결 방법
tw-merge 공식 문서를 살펴보면, 커스텀 Tailwind config를 사용중일 경우 클래스를 제대로 병합하기 위해 tailwind-merge를 구성해야 할 수도 있다고 합니다.
Tailwind-merge config는 다양한 설정을 지원하지만, 우리는 단순히 확장된 클래스를 tailwind-merge에게 알려주면 됩니다. 이때 사용할 수 있는 것이 extendTailwindMerge
입니다.
const customTwMerge = extendTailwindMerge({
extend: {
classGroups: {
'font-size': ['text-caption'],
}
}
})
text-caption
이외에 다른 커스텀 폰트 사이즈가 있다면 다음과 같이 작성할 수도 있습니다.
const customTwMerge = extendTailwindMerge({
extend: {
classGroups: {
'font-size': [{ text: ['caption', 'base'] }],
}
}
})
같은 prefix를 사용한다면 위처럼 묶어서 작성할 수 있습니다.
이렇게 확장된 twMerge
로 기존에 사용하던 twMerge
를 대체하면 기대한 대로 병합이 이루어지는 것을 확인할 수 있습니다.
<!-- Button 컴포넌트 -->
<button
className={customTwMerge('rounded bg-primary px-1 py-2 text-white', className)}
>
{children}
</button>


마치며
최근 프로젝트를 모두 Tailwind CSS로 진행하면서 지속적으로 해당 문제를 경험했었습니다.
처음에는 프로젝트 막바지에 문제를 맞닥뜨려서 원인 파악조차 하지 못한 채 넘어갔습니다. 두 번째가 되어서야 공식 문서를 살펴보았는데, 해당 문제에 대해 상세하게 설명하고 있어 바로 문제를 해결할 수 있었습니다. 문서를 보면서 tailwind-merge를 사용해야 하는 이유, 사용하지 않아도 되는 이유 또한 볼 수 있어 좋았습니다.
급할수록 돌아가라는 말처럼, 새로운 라이브러리를 도입할 때는 공식 문서를 꼼꼼히 살펴보는 습관을 들여야겠습니다😄