Tailwind CSS 잘 활용하기 (feat. clsx, twMerge, cva)

어진·2025년 12월 5일

WEB

목록 보기
4/6
post-thumbnail

합세 때 스타일 초기 세팅을 담당하면서 Tailwind CSS를 보다 잘 활용하기 위한 필수 라이브러리인 clsx, twMerge, cva에 대해 알게 되었습니다. 그 과정에서 배운 내용을 나중에 보다 효율적으로 활용해 보고자 정리해보았습니다!


💡 왜 필요한가

Tailwind CSS는 유틸리티 클래스를 조합하여 스타일을 만드는 방식이라, 프로젝트 규모가 커질수록 아래 문제가 발생하기 쉽습니다.

  • 상태별 조건부 스타일 → className이 지나치게 복잡해짐
  • p-4 vs p-8 같은 클래스 충돌 → 최종 적용 스타일 예측 어려움
  • button / input처럼 variant, size, state가 많은 컴포넌트 확장 어려움

이 세 가지를 해결해 주는 도구가 바로 clsx, twMerge, cva입니다!

 

🛠️ 설치

# clsx
npm install clsx

# twMerge
npm install tailwind-merge

# cva
npm install class-variance-authority

 

🧩 clsx

클래스명을 조건부/동적으로 깔끔하게 조합할 수 있도록 도와주는 라이브러리입니다.

  • 주요 기능: 문자열, 객체, 배열 등 다양한 형태의 입력값을 받아 하나의 공백으로 구분된 클래스 문자열로 결합합니다.
  • 장점:
    • undefined, null, false 값을 자동으로 제거합니다.
    • 조건부 스타일링을 객체 문법으로 간결하게 표현할 수 있습니다.
import clsx from "clsx";

export function Button() {
  const isActive = true;
  const isDisabled = false;

  return (
    <button
      className={clsx(
        "px-4 py-2", // 기본 클래스
        {
          "bg-blue-500 text-white": isActive, // 조건부 클래스
          "opacity-50 cursor-not-allowed": isDisabled,
        }
      )}
      disabled={isDisabled}
    >
      버튼
    </button>
  );
}

 

🧩 twMerge (tailwind-merge)

중복되거나 충돌하는 Tailwind 클래스를 자동으로 병합하는 라이브러리입니다.

  • 충돌하는 클래스가 있는 경우 마지막에 선언된 클래스가 우위를 가집니다.
import { twMerge } from "tailwind-merge";

export function Button({ className }: { className?: string }) {
  return (
    <button
      className={twMerge(
        className,                                // 외부에서 전달된 className
        "rounded-md bg-blue-500 p-2 text-white"   // 기본 스타일(뒤에서 선언 → 우선 적용)
      )}
    >
      버튼
    </button>
  );
}
// 사용: <Button className="bg-red-500 p-6" />
// 결과: "rounded-md bg-blue-500 p-2 text-white"

 

🧩 cn (clsx + twMerge)

cn은 별도의 라이브러리가 아닌 clsx와 twMerge를 결합하여 사용하는 유틸 함수입니다.

  • 조건부 렌더링과 클래스 충돌 방지를 한 번에 해결할 수 있습니다.

 
유틸 함수 생성

// utils/cn.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
	return twMerge(clsx(...inputs));
}

활용 방법

// 기본은 파랑, warning 상태일 땐 노랑으로 덮어쓰기, disabled면 커서 비활성 처리
<button
  className={cn(
    "bg-blue-500 text-white px-4 py-2 rounded-md", // 기본 스타일
    isWarning && "bg-yellow-400 text-black",       // 경고 상태 → 배경/텍스트 컬러 변경
    isDisabled && "cursor-not-allowed opacity-60"  // 비활성화 → 클릭 불가 + 흐리게
  )}
/>

 

🧩 cva (class-variance-authority) ⭐️ ⭐️

참고 🔗 https://cva.style/docs

컴포넌트에 적용되는 다양한 스타일을 체계적으로 매핑할 수 있게 도와주는 라이브러리입니다.
variant, size, 상태 등 여러 스타일 조합을 한 곳에서 정의하고 props로 제어할 수 있어, 확장 가능한 UI 컴포넌트 제작에 최적화되어 있습니다.

 

// styles/button.ts
import { cva } from "class-variance-authority";

export const buttonVariants = cva(
  // base: 모든 버튼에 공통으로 적용될 스타일
  "inline-flex items-center justify-center rounded-md font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed",
  {
    // variants: props로 제어할 수 있는 스타일 그룹
    variants: {
      variant: {
        // variant 종류별 스타일
        primary: "bg-blue-500 text-white hover:bg-blue-600",
        secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300",
      },
      // size별 스타일
      size: {
        sm: "px-2 py-1 text-xs",
        md: "px-4 py-2 text-sm",
        lg: "px-6 py-3 text-base",
      },
    },
    // 특정 옵션 조합일 때만 추가 스타일 적용
    compoundVariants: [
      {
        variant: "primary",
        size: "lg",
        className: "shadow-lg", // 큰 기본 버튼에만 섀도우 추가
      },
      {
        variant: "secondary",
        size: "sm",
        className: "border border-gray-400", // 작은 보조 버튼에만 테두리 추가
      },
    ],
    // props가 전달되지 않았을 때 적용할 기본 옵션
    defaultVariants: {
      variant: "primary",
      size: "md",
    },
  }
);

활용 방법

<Button>기본 버튼</Button>
<Button variant="secondary">보조 버튼</Button>
<Button variant="primary" size="lg">큰 기본 버튼</Button>

props 조합에 따라 buttonVariants()가 자동으로 클래스 문자열을 생성해주기 때문에, 컴포넌트 내부에서 조건문으로 className을 분기할 필요가 없습니다.

 
추가 설정

기본 설정 그대로 사용하면, cva() 함수 내부에서는 Tailwind CSS Intellisense(자동완성)가 잘 안 붙는 경우가 있습니다.
따라서 VSCode의 settings.json에서 cva 패턴을 인식하도록 아래 정규식을 추가해주면, cva() 안에서도 Tailwind 클래스 자동완성을 그대로 사용할 수 있습니다.

"tailwindCSS.experimental.classRegex": [
  ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
]

 
cva는 cn이랑 같이 많이 사용하고, storybook이랑도 호환이 좋다고 하니 유용하게 사용할 수 있을 것 같습니다!

 

💡 핵심 요약

함수역할핵심 기능
clsx조건부 클래스 조합상태에 따라 클래스를 선택적으로 붙일 때 사용
twMergeTailwind 클래스 충돌 정리중복된 Tailwind 속성 중 마지막 값을 자동으로 적용
cn조건부 + 충돌 처리 통합clsx + twMerge를 한 번에 쓰기 위한 유틸 함수
cva컴포넌트 스타일 변형 관리variant / size / 상태 등을 규칙으로 정의해 props 기반으로 적용
profile
👩🏻‍💻

0개의 댓글