본문 바로가기
C.W.K.
Stream
Lesson 04 of 07 · published

tailwind-merge + clsx — cn() 헬퍼

~11 min · tailwind-merge, clsx, cn, className

Level 0React 입문자
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
React UI에는 조건부 className을 합치는 작은 cn() 헬퍼가 자주 필요해. 단순 연결과 충돌 해결이 왜 다른 문제인지 알아보자.

cn()이 해결하는 두 가지 문제

  1. 조건부 클래스: 기본 클래스에 특정 prop이 켜졌을 때만 클래스를 더하고 싶을 수 있어. 이런 조건이 늘어나면 단순 템플릿 리터럴은 금방 읽기 어려워져.
  2. 호출자의 재정의: Button이 내부에서 px-4를 쓰는데 호출자가 px-6를 넘기면 DOM에는 두 클래스가 모두 남아. CSS가 로드된 순서에 따라 예상과 다른 값이 적용될 수 있어.

두 라이브러리의 역할

  • clsx: 문자열, 배열, 조건부 객체를 받아 적용할 클래스만 공백으로 이어 줘.
  • tailwind-merge: Tailwind 유틸리티의 의미를 알고 충돌하는 클래스를 정리해. 예를 들어 px-4px-6를 함께 받으면 뒤의 px-6만 남겨.

두 함수를 합친 cn()을 프로젝트의 공용 헬퍼로 두면 조건 조합과 Tailwind 충돌 해결을 한곳에서 처리할 수 있어.

cn()을 둘 자리

src/lib/cn.ts에 한 번 만들고 모든 컴포넌트에서 가져다 써. cwkPippa가 tailwind-merge를 사용하는 이유도 같아. 채팅 UI, 사이드바, council, admin 패널이 하나의 cn() 계약을 공유하면 각 컴포넌트가 문자열 병합 규칙을 다시 만들 필요가 없어.

클래스 문자열을 +나 템플릿 리터럴로 계속 이어 붙이고 있다면 cn()을 다시 만들고 있는 셈이야. 두 라이브러리는 작고 역할이 분명해. 한 번 조합한 공용 헬퍼를 두고 다음 문제로 넘어가.

두 라이브러리의 책임이 달라

clsx는 조건에 따라 문자열을 골라 합치고, tailwind-merge는 px-4 px-8처럼 같은 역할의 Tailwind 유틸리티가 충돌할 때 뒤의 값을 남겨. cn()은 두 단계를 한 함수로 묶어.

Code

공용 cn.ts 파일·ts
// src/lib/cn.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]): string {
  return twMerge(clsx(inputs));
}
조건부 클래스와 호출자 재정의를 지원하는 컴포넌트·tsx
import { cn } from "@/lib/cn";

type ButtonProps = {
  variant?: "primary" | "ghost";
  size?: "sm" | "md" | "lg";
  disabled?: boolean;
  className?: string;
  children: React.ReactNode;
};

export function Button({ variant = "primary", size = "md", disabled, className, children }: ButtonProps) {
  return (
    <button
      disabled={disabled}
      className={cn(
        // 기본 클래스 — 항상 적용
        "inline-flex items-center justify-center rounded-lg font-medium transition-colors",
        // 종류 — 하나 선택
        {
          "bg-brand text-bg hover:bg-brand-strong": variant === "primary",
          "bg-transparent text-fg hover:bg-bg-elevated": variant === "ghost",
        },
        // 크기 — 하나 선택
        {
          "px-3 py-1.5 text-sm": size === "sm",
          "px-4 py-2 text-base": size === "md",
          "px-6 py-3 text-lg": size === "lg",
        },
        // 비활성 상태
        disabled && "opacity-50 cursor-not-allowed",
        // 호출자 재정의 — tailwind-merge가 충돌을 해결해
        className
      )}
    >
      {children}
    </button>
  );
}

// 호출자가 패딩을 재정의해도 충돌하지 않아:
// <Button size="md" className="px-8">Bigger</Button>
// → tailwind-merge가 px-4(size=md)를 제거하고 px-8을 유지해.

External links

Exercise

부트스트랩 프로젝트에 clsx와 tailwind-merge를 설치하고 src/lib/cn.ts에 cn helper를 만들어. className prop을 받는 기존 Card가 cn(internalClasses, className)을 사용하도록 바꾼 뒤 호출자가 넘긴 충돌 유틸리티가 이기는지 확인해.
Hint
Card의 기본 배경이 bg-bg-elevated라면 <Card className="bg-red-500">를 렌더해 봐. 최종 class 문자열에는 충돌하지 않는 클래스와 호출자의 배경 클래스만 남아야 해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.