문제 정의
<div className={twMerge("bg-[#333] bg-opacity-[20%]")}>dd</div>
bg와 bg-opacity는 같은 속성이 아닌데, 하나로 합쳐진다.
사실 수집
스타일에는 두 속성이 모두 존재한다. 왜냐하면, TailwindCSS은 컴파일 과정에서 미리 스타일을 변환하기 때문이다. (JIT로 인해 수정 후 저장 시에도 즉시 적용이 된다)
원인 추론
twMerge코드를 살펴보자.
mergeClassList.ts에서 주어진 class들을 합친다.
테스트
클론 후 테스트를 해보았다.
// twMerge.test.ts
expect(twMerge('bg-[#333] bg-opacity-[20%]')).toBe('bg-[#333] bg-opacity-[20%]')
역시 fail이다.
console을 찍어보니, originalClassName은 분명히 다른데 classGroupId는 동일하게 bg-color가 적용이 되었다. 파싱하는 과정에서 무언가 문제가 있던 것이다.
let classGroupId = getClassGroupId(
hasPostfixModifier
? baseClassName.substring(0, maybePostfixModifierPosition)
: baseClassName,
)
우리가 찾아야할 건 classGroupId. 이 값을 만드는 건 getClassGroupId함수.
const { parseClassName, getClassGroupId, getConflictingClassGroupIds } = configUtils
이 getClassGroupId함수는 configUtils객체에 속한다.
이 configUitls는 createConfigUtils함수에서 만들어온다.
// create-tailwind-merge.ts
configUtils = createConfigUtils(config)
// config-utils.ts
export const createConfigUtils = (config: GenericConfig) => ({
cache: createLruCache<string, string>(config.cacheSize),
parseClassName: createParseClassName(config),
...createClassGroupUtils(config),
})
// create-group-utils.ts
export const createClassGroupUtils = (config: GenericConfig) => {
const getClassGroupId = (className: string) => {
const classParts = className.split(CLASS_PART_SEPARATOR)
// Classes like `-inset-1` produce an empty string as first classPart. We assume that classes for negative values are used correctly and remove it from classParts.
if (classParts[0] === '' && classParts.length !== 1) {
classParts.shift()
}
return getGroupRecursive(classParts, classMap) || getGroupIdForArbitraryProperty(className)
}
!찾았다 요놈
getGroupRecursive이 함수가 형을 하고 있었다.
// create-group-utils.ts
const getGroupRecursive = (
위처럼 20에서는 인식을 못하지만 opacity-20에서는 bg-opacity임을 인식한다.
하지만 여기서는 opacity-[20%]에서 인식을 하지 못하여 bg-opacity-[20%]까지 오게 되어, 가장 맨 앞의 bg에 의해 결정이 되어 bg-color라는 classId를 가지게 된다.
…
하던 중
7시간 전에 추가된 코멘트를 확인했다.
또한, 그로부터 몇시간 후 버그가 픽스되었다.
getNumberAndArbitrary함수는 다음과 같다.
const getNumberAndArbitrary = () => [isNumber, isArbitraryValue]
export const isNumber = (value: string) => Boolean(value) && !Number.isNaN(Number(value))
export const isArbitraryValue = (value: string) => arbitraryValueRegex.test(value)
const arbitraryValueRegex = /^\[(?:([a-z-]+):)?(.+)\]$/i
즉, isNumber은 Number인 지 체크하는 함수이고, isArbitraryValue는 [a] 혹은 [a:b]같이 대괄호 사이에 문자가 있는 임의의 값을 체크하는 함수이다. 기존에는 Number인지만 체크했기 때문에 bg-opacity-[20%]같은 것을 확인하지 못했던 것이다. 그리고, 이를 수정한 것이었다.
정리
내가 직접 해결까지 하지는 못해서 아쉬웠지만, 버그의 원인을 찾아가는 디버깅 과정이 정말 흥미로웠다. 거의 원인을 찾기 까지 근접했다는 것이 이번 경험은 매우 만족스럽다. 앞으로, 내가 주로 사용하는 기술의 이슈들을 확인해서 내가 직접 수정해보는 것도 반드시 좋은 경험이 될 듯하다.