- Published on
트러블 슈팅 - 테일윈드머지 커스텀 클래스 병합 문제
- Authors
- Name
- 정윤호
배경
twMerge 는 tailwind 의 유틸리티 클래스명 중에 같은 속성에 해당하는 클래스명이 여럿 존재하는 경우 일련의 규칙에 따라 하나의 클래스명만 남기는 기능을 제공한다. 보통 상위에서 props 로 전달받는 클래스명이 기존 동일 속성의 클래스명을 override 하는 목적으로 많이 쓰인다.
문제상황
Chip 컴포넌트 개발 중에 jest 로 단위 테스트를 돌리던 중, props 로 전달내린 클래스명이 적절하게 전달 받는지에 대한 테스트 케이스가 통과하지 못한 사실을 발견했다.
위에서 보이듯이 text-gray-500
이라는 클래스명이 제대로 전달 받고있지 않다.
이 문제는 이전 taskify 에서도 경험한 문제였다. 그 원인을 추측컨대, text-
prefix 를 폰트 사이즈와 텍스트 색상 클래스가 공유하는 가운데 twMerge 가 이 둘이 다른 속성임을 알지 못해 text-body3
가 text-gray-500
을 덮어씌워버린 것 같다.
문제 원인
검색 결과 흔히 나타나는 문제인 것 같다. 알아낸 점은 tailwind.config.ts 에서 새로 클래스명을 확장하지 않은 경우에는 정상적으로 text-[폰트사이즈]
와 test-[text 색상]
이 공존할 수 있으나, 확장된 커스텀 클래스의 경우 이런 문제가 발생한다는 것이다.
// tailwind.config.ts
const fontPalette: Record<
string,
[string, { lineHeight: string; letterSpacing: string }] | string
> = {
heading1: ['40px', { lineHeight: '54px', letterSpacing: '-0.02em' }],
heading2: ['28px', { lineHeight: '36px', letterSpacing: '-0.02em' }],
heading3: ['24px', { lineHeight: '32px', letterSpacing: '-0.02em' }],
heading4: ['22px', { lineHeight: '30px', letterSpacing: '-0.02em' }],
heading5: ['20px', { lineHeight: '28px', letterSpacing: '-0.02em' }],
title1: ['18px', { lineHeight: '26px', letterSpacing: '-0.02em' }],
title2: ['16px', { lineHeight: '24px', letterSpacing: '-0.02em' }],
body1: ['16px', { lineHeight: '24px', letterSpacing: '-0.02em' }],
body2: ['15px', { lineHeight: '22px', letterSpacing: '-0.02em' }],
body3: ['14px', { lineHeight: '20px', letterSpacing: '-0.02em' }],
caption1: ['13px', { lineHeight: '18px', letterSpacing: '-0.02em' }],
caption2: ['12px', { lineHeight: '16px', letterSpacing: '-0.02em' }],
inherit: 'inherit',
}
위는 dfd 프로젝트에서 팀원이 설정해준 폰트 크기 관련 커스텀 클래스명이다.
twMerge 메커니즘
https://github.com/dcastil/tailwind-merge/blob/main/src/lib/merge-configs.ts https://github.com/dcastil/tailwind-merge/blob/main/src/lib/default-config.ts
twMerge('p-4 p-2 text-lg text-sm')
twMerge 가 제공하는 기능은 단순하다.
- 클래스명을 파싱하고
['p-4', 'p-2', 'text-lg', 'text-sm']'
- tailwind css 클래스 그룹화 규칙을 따라 같은 그룹의 클래스명을 비교한다.
- p-4 vs p-2
- text-lg vs text-sm
- 같은 그룹에 속한 클래스가 충돌할 경우, 가장 마지막에 선언된 클래스를 우선적으로 적용한다.
p-2 text-sm
우승
type DefaultThemeGroupIds = 'blur' | 'borderColor' | 'borderRadius' | 'borderSpacing' | 'borderWidth' | 'brightness' | 'colors' | 'contrast' | 'gap' | 'gradientColorStopPositions' | 'gradientColorStops' | 'grayscale' | 'hueRotate' | 'inset' | 'invert' | 'margin' | 'opacity' | 'padding' | 'saturate' | 'scale' | 'sepia' | 'skew' | 'space' | 'spacing' | 'translate';
type DefaultClassGroupIds = 'accent' | 'align-content' | 'align-items' | 'align-self' | 'animate' | 'appearance' | 'aspect' | 'auto-cols' | 'auto-rows' | 'backdrop-blur' | 'backdrop-brightness' | ... `
테마와 클래스 그룹으로 같은 속성의 클래스명인지 구분한다. font-size 와 text-color 도 별개의 그룹으로 구분되어진다.
다만, twMerge 는 기본적으로 tailwind css 의 그룹화 규칙을 따르기 때문에 커스텀 클래스명의 경우 중복 제거 동작 시 예상치 못한 동작을 보일 수 있다.
즉, 기존의 text-heading1 등의 클래스명이 font-size 그룹으로 인정되지 않기 때문에, text-color 그룹의 text-gray-500 을 오버라이드한 것이다.
해결
https://happynet-fe.tistory.com/1 https://github.com/dcastil/tailwind-merge/blob/v2.4.0/docs/configuration.md https://github.com/dcastil/tailwind-merge/blob/main/src/lib/extend-tailwind-merge.ts
해결 방안에 대해서도 검색을 해보았다. 블로그 글에서 소개되는 방식은 커스텀 클래스명을 tailwind 에서 사용중이라면, 기존의 merge-config 에 새로운 규칙을 추가하면 된다.
export const twMergeEx = extendTailwindMerge({
extend: {
classGroups: {
'font-size': [
{
text: [
'heading1',
'heading2',
'heading3',
'heading4',
'heading5',
'title1',
'title2',
'body1',
'body2',
'body3',
'caption1',
'caption2',
'inherit',
],
},
],
},
},
})
extendTailwindMerge 메서드를 이용해 새로운 규칙을 확장한 twMergeEx 메서드를 생성할 수 있다.
테스트
const mergeText = 'text-gray-500 text-heading1'
console.log(`기존 twMerge : ${twMerge(mergeText)}`)
console.log(`기존 twMergeEx : ${twMergeEx(mergeText)}`)
의도한대로 twMergeEx 에서는 같은 text-
prefix 를 공유하더라도 이 두 클래스명을 다른 그룹으로 처리한다.