clsx vs tailwind merge (cn)
Tailwind CSS 클래스 관리 라이브러리 비교
Tailwind CSS로 프로젝트를 진행하다 보면 cn 함수를 정말 많이 쓰게 됩니다. clsx를 사용할 때보다, 스타일 충돌이 없어서 별 생각 없이 계속 사용했는데, 문득 이게 정확히 어떻게 동작하는지 궁금해졌습니다.
사실 cn은 두 가지 라이브러리의 조합입니다:
clsx: 조건부 클래스명을 깔끔하게 관리해주는 유틸리티tailwind-merge: Tailwind CSS 클래스 충돌을 해결해주는 라이브러리
특히 tailwind-merge가 재미있습니다. px-4와 px-6이 함께 있으면 자동으로 중복을 제거하고 마지막 값만 남겨줍니다. 그래서 컴포넌트를 사용할 때 스타일을 쉽게 덮어쓸 수 있었던 거죠.
clsx의 코드 동작
clsx의 소스 코드를 보면, 단순한 일을 합니다:
- 문자열, 객체, 배열 등 다양한 형태의 인자를 받아서
- falsy 값은 무시하고
- truthy 값만 공백으로 연결해 하나의 문자열로 반환
그래서 조건부 클래스명을 깔끔하게 관리할 수 있게 해주지만, 단순히 문자열을 합치는 역할만 합니다.
clsx의 한계
clsx는 훌륭하지만, Tailwind CSS와 함께 쓸 때 한 가지 문제가 있습니다:
const buttonClass = "px-4 py-2 bg-blue-500"
<button className={clsx(buttonClass, "px-6")} />
// 결과: "px-4 py-2 bg-blue-500 px-6"
이렇게 되면 px-4와 px-6 두 클래스가 모두 className에 들어갑니다. 브라우저는 두 클래스를 모두 인식하지만, 실제로 어떤 패딩이 적용될지는 예측하기 어렵죠.
Tailwind CSS는 클래스 순서가 아닌, CSS 파일에서의 정의 순서에 따라 우선순위가 결정됩니다. 같은 카테고리 내에서는 Tailwind가 정한 내부 순서대로 정의되어 있어서 class="px-2 px-6"이든 class="px-6 px-2"이든 우리가 정의한 순서와는 관련없이 CSS 파일에서 나중에 정의된 클래스가 항상 적용됩니다.
tailwind-merge의 해결책
이 문제를 해결하기 위해 tailwind-merge가 만들어졌습니다:
import { twMerge } from "tailwind-merge"
twMerge("px-4 py-2 bg-blue-500", "px-6")
// → "py-2 bg-blue-500 px-6" (px-4가 제거됨!)
어떻게 충돌을 감지할까?
tailwind-merge의 소스 코드를 보면, Tailwind의 모든 클래스를 그룹으로 분류해두고, 어떤 그룹이 충돌하는지도 정의해두었습니다:
// padding 관련 클래스 그룹 정의
classGroups: {
p: [/* padding */],
px: [/* padding-x */],
...
}
// 충돌하는 클래스 정의
conflictingClassGroups: {
p: ['px', 'py', 'ps', 'pe', 'pt', 'pr', 'pb', 'pl'],
px: ['pr', 'pl'],
py: ['pt', 'pb'],
}
이후, 병합 과정은 mergeClassList에서 일어납니다. 이 때 for loop를 뒤에서부터 순회하기 때문에, 나중에 작성한 클래스가 우선순위를 갖게 됩니다.
언제 무엇을 사용해야 할까?
clsx 사용
- 성능이 중요한 경우 (번들 사이즈 228B)
- 클래스 충돌이 없다고 확신할 때
cn (tailwind-merge) 사용
- 컴포넌트에 className prop을 받아 스타일 오버라이드가 필요할 때
- 동적으로 Tailwind 클래스를 조합할 때
대부분의 Tailwind 프로젝트에서는 cn을 사용하는 것이 안전합니다.