Skip to content

Tailwind에서 clsx, tailwind-merge, cva를 함께 쓰는 이유

이전 프로젝트를 진행할 때, cn 유틸 함수에 대해 정리하려 했지만 미루게 되었고...
이번 스프린트 과정에서 Tailwind를 처음부터 다시 학습하며 새롭게 이해한 내용들을 정리해보려 한다.


Tailwind CSS로 컴포넌트를 만들다 보면 자연스럽게 이런 문제를 만나게 된다.

  • 조건부 스타일이 복잡해진다.
  • 클래스 충돌로 인해 의도한 오버라이딩이 동작하지 않는다.
  • 점점 복잡해지는 className

이러한 문제를 해결하기 위해 자주 사용되는 도구가 clsx, tailwind-merge, 그리고 cva다.


clsx - 조건부 클래스 관리

props에 따라 스타일이 달라져야 한다.
예를 들어 버튼만 해도, primary, secondary, outline, disabled 등 존재한다.

tsx
className={`
  ${variant === "primary" && "..."}
  ${variant === "secondary" && "..."}
`}

조건이 늘어날수록 코드가 복잡해진다.


clsx의 역할

clsx는 조건부 클래스를 훨씬 깔끔하게 정리해준다.

tsx
clsx(
  'base-class',
  variant === 'primary' && 'bg-blue-500',
  isDisabled && 'opacity-50',
);
  • 조건부 처리 가능
  • falsy 값 자동 무시
  • 가독성 개선

clsx만으로 해결되지 않는 문제

하지만 Tailwind에서는 또 다른 문제가 있다.
동일한 속성 그룹의 클래스가 함께 존재할 수 있기 때문이다.

tsx
bg-gray-600 bg-gray-400

위와 같이 background-color 그룹의 클래스가 동시에 적용되면, clsx는 이를 단순히 병합만 수행한다. 즉, 어떤 클래스를 우선 적용해야 하는지는 판단하지 않는다.


tailwind-merge - 충돌 클래스 해결

tailwind-merge는 Tailwind 전용 충돌 해결 도구다.

tsx
twMerge('bg-gray-600 bg-gray-400');

// 결과: bg-gray-400
  • 같은 그룹 클래스 자동 정리
  • 마지막 값만 유지
  • 오버라이딩 안정성 확보

그래서 등장하는 cn 함수

이전에 프로젝트를 진행할 때 유틸 함수를 만들어 사용했다.
Tailwind를 사용하는 환경에서는 사실상 표준 패턴처럼 사용된다.

ts
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs) {
  return twMerge(clsx(inputs));
}

cn은 새로운 라이브러리가 아니라,
clsxtailwind-merge를 함께 사용하기 위한 유틸 함수다.

  • clsx로 조건부 클래스를 정리하고
  • tailwind-merge로 충돌하는 Tailwind 클래스를 정리한 뒤 최종 className 문자열을 반환한다.

그런데 컴포넌트가 커지면 또 생기는 문제

tailwind-mergecn 조합을 사용하면 클래스 충돌 문제는 비교적 깔끔하게 해결할 수 있다.
하지만 컴포넌트의 스타일 옵션이 늘어나기 시작하면 여전히 또 다른 문제가 발생한다.

바로 조건부 클래스 로직의 복잡도다.

tsx
clsx(
  size === 'sm' && '...',
  size === 'md' && '...',
  variant === 'primary' && '...',
  variant === 'secondary' && '...',
  disabled && '...',
  loading && '...',
);

옵션이 증가할수록 조건문이 빠르게 비대해지고, 스타일 구조를 한눈에 파악하기 어려워진다.


cva - 스타일 구조 설계

class-variance-authority cva는 접근 방식 자체가 다르다.
조건문을 작성하는 방식이 아니라, 스타일 경우의 수를 선언적으로 정의한다.

tsx
const buttonVariants = cva('base-style', {
  variants: {
    variant: {
      primary: '...',
      secondary: '...',
    },
    size: {
      sm: '...',
      lg: '...',
    },
  },
  defaultVariants: {
    variant: 'primary',
    size: 'sm',
  },
});

className 로직을 작성하는 것이 아니라, 스타일 시스템을 설계하는 방식에 가깝다.

tsx
<button className={cn(buttonVariants({ variant, size }))} />

🧩 정리

도구역할
clsx조건부 클래스 정리, falsy 값 무시
tailwind-merge같은 속성 클래스 충돌 해결, 오버라이딩 지원
cn위 둘을 합친 헬퍼 함수
cvavariant 기반으로 스타일 시스템 구조화

마지막 업데이트 날짜: