๐Ÿงฉ Tailwind 11์žฅ: ์ปดํฌ๋„ŒํŠธ ํŒจํ„ด๊ณผ ์žฌ์‚ฌ์šฉ ์„ค๊ณ„

2026๋…„ 3์›” 5์ผ ์ˆ˜์ •๋จ

๐Ÿ“‹ ๊ฐœ์š”

cva, tailwind-merge ๋กœ ํƒ€์ž… ์•ˆ์ „ํ•œ ์žฌ์‚ฌ์šฉ ์ปดํฌ๋„ŒํŠธ ๋งŒ๋“ค๊ธฐ โ€” ๋ณ€ํ˜•(variant) ๊ธฐ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ ์„ค๊ณ„์˜ ์ •์„

๐Ÿ“‹ ๋ชฉ์ฐจ


๐Ÿ“Œ ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ธฐ ์ „์—

โฑ๏ธ ์˜ˆ์ƒ ์ฝ๊ธฐ ์‹œ๊ฐ„: 20๋ถ„

๐ŸŽฏ ์ด ๋ฌธ์„œ๋ฅผ ๋‹ค ์ฝ์œผ๋ฉด ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ

  • tailwind-merge ๋กœ ํด๋ž˜์Šค ์ถฉ๋Œ ์—†์ด ์ปดํฌ๋„ŒํŠธ ์Šคํƒ€์ผ์„ ์˜ค๋ฒ„๋ผ์ด๋“œํ•  ์ˆ˜ ์žˆ๋‹ค
  • cva ๋กœ ํƒ€์ž… ์•ˆ์ „ํ•œ ๋ณ€ํ˜• ๊ธฐ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์„ค๊ณ„ํ•  ์ˆ˜ ์žˆ๋‹ค
  • Button, Badge, Input, Card ๊ฐ™์€ ๊ธฐ๋ณธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‹œ๋‹ˆ์–ด ์ˆ˜์ค€์œผ๋กœ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค

๐Ÿค” ์™œ ์•Œ์•„์•ผ ํ•˜๋Š”๊ฐ€

์˜์ฒ ์ด๊ฐ€ Button ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ด๋ ‡๊ฒŒ ๋งŒ๋“ค์—ˆ์–ด:

// ๐Ÿฃ ์˜์ฒ ์˜ ์ˆœ์ง„ํ•œ ์ ‘๊ทผ
function Button({ className, children }: { className?: string; children: React.ReactNode }) {
  return (
    <button className={`bg-blue-600 text-white px-6 py-3 rounded-xl ${className}`}>
      {children}
    </button>
  );
}
 
// ์‚ฌ์šฉ ์‹œ ํฌ๊ธฐ๋ฅผ ๋ฐ”๊พธ๊ณ  ์‹ถ์–ด์„œ...
<Button className="px-3 py-1.5">์ž‘์€ ๋ฒ„ํŠผ</Button>

๊ฒฐ๊ณผ? className ์ด bg-blue-600 text-white px-6 py-3 rounded-xl px-3 py-1.5 โ€” px-6 ๊ณผ px-3 ์ด ๋™์‹œ์— ์„ ์–ธ๋ผ๋ฒ„๋ ค. CSS ์—์„œ๋Š” ๋‚˜์ค‘์— ์˜ค๋Š” ๊ฒŒ ์ด๊ธฐ๋Š”๋ฐ, ์–ด๋А ์ชฝ์ด ๋‚˜์ค‘์ธ์ง€๋Š” Tailwind ํด๋ž˜์Šค ์ •์˜ ์ˆœ์„œ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ ธ์„œ ์˜ˆ์ธก ๋ถˆ๊ฐ€์•ผ.

์ด๊ฒŒ ๋ฐ”๋กœ ํด๋ž˜์Šค ์ถฉ๋Œ ๋ฌธ์ œ์ด๊ณ , tailwind-merge ์™€ cva ๊ฐ€ ํ•ด๊ฒฐํ•ด.


โš ๏ธ ํด๋ž˜์Šค ์ถฉ๋Œ ๋ฌธ์ œ: tailwind-merge

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • ์™œ ๋ฌธ์ž์—ด ์—ฐ๊ฒฐ(+ ๋˜๋Š” ํ…œํ”Œ๋ฆฟ ๋ฆฌํ„ฐ๋Ÿด)๋กœ ํด๋ž˜์Šค๋ฅผ ํ•ฉ์น˜๋ฉด ์œ„ํ—˜ํ•œ์ง€ ์›๋ฆฌ๋กœ ์ดํ•ดํ•œ๋‹ค
  • twMerge ๊ฐ€ CSS ์บ์Šค์ผ€์ด๋”ฉ์„ ์–ด๋–ป๊ฒŒ ์ œ์–ดํ•˜๋Š”์ง€ ์•Œ ์ˆ˜ ์žˆ๋‹ค
  • cn() ํ—ฌํผ ์œ ํ‹ธ๋ฆฌํ‹ฐ๋ฅผ ํŒ€ ๊ณตํ†ต ์œ ํ‹ธ๋กœ ๋“ฑ๋กํ•˜๋Š” ํŒจํ„ด์„ ์ตํžŒ๋‹ค

๐Ÿฆ ์˜ํ˜ธ: "์˜์ฒ  ๋‹˜, ๋ฐฉ๊ธˆ ๋งŒ๋“  ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ์—์„œ px-3 ์ด ์ ์šฉ์ด ์•ˆ ๋˜๋Š” ๊ฑฐ ๋ณด์ด์ฃ ? ์›์ธ์€ Tailwind ํด๋ž˜์Šค ์ถฉ๋Œ์ด์—์š”. tailwind-merge ๋ฅผ ์“ฐ๋ฉด ๊ฐ„๋‹จํ•˜๊ฒŒ ํ•ด๊ฒฐ๋ผ์š”."

tailwind-merge ๋Š” ์ค‘๋ณต๋˜๋Š” Tailwind ํด๋ž˜์Šค๋ฅผ ์ง€๋Šฅ์ ์œผ๋กœ ๋ณ‘ํ•ฉํ•ด์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ. ๋‹จ์ˆœํžˆ ๋ฌธ์ž์—ด์„ ๋ถ™์ด๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, Tailwind ํด๋ž˜์Šค๋“ค์ด ์„œ๋กœ ๊ฐ™์€ CSS ์†์„ฑ์„ ์ œ์–ดํ•˜๋Š”์ง€ ํŒŒ์•…ํ•˜๊ณ  ๋งˆ์ง€๋ง‰์— ์˜ค๋Š” ํด๋ž˜์Šค๋ฅผ ์šฐ์„ ์‹œํ•ด์ค˜.

npm install tailwind-merge

์„ค์น˜ ํ›„, ๋™์ž‘ ์›๋ฆฌ๋ฅผ ์ฝ”๋“œ๋กœ ์ง์ ‘ ํ™•์ธํ•ด๋ด:

import { twMerge } from 'tailwind-merge';
 
// โŒ ์ผ๋ฐ˜ ๋ฌธ์ž์—ด ํ•ฉ์น˜๊ธฐ โ€” px ์ถฉ๋Œ ๋ฐœ์ƒ, ์–ด๋А ์ชฝ์ด ์ ์šฉ๋ ์ง€ ์˜ˆ์ธก ๋ถˆ๊ฐ€
const badResult = `px-6 py-3 ${className}`;  // โ†’ "px-6 py-3 px-3 py-1"
 
// โœ… twMerge โ€” ๋™์ผ CSS ์†์„ฑ ์ œ์–ด ํด๋ž˜์Šค ์ค‘ ๋งˆ์ง€๋ง‰ ๊ฐ’์ด ์ด๊น€
twMerge('bg-blue-500', 'bg-red-500')    // โ†’ "bg-red-500" (bg ์ถฉ๋Œ โ†’ ๋’ค๊ฐ€ ์ด๊น€)
twMerge('p-4', 'px-6')                  // โ†’ "p-4 px-6"  (p ์™€ px ๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ์†์„ฑ!)
twMerge('p-4', 'p-6')                   // โ†’ "p-6"       (p ์ถฉ๋Œ โ†’ ๋’ค๊ฐ€ ์ด๊น€)
twMerge('font-bold text-sm', 'text-lg') // โ†’ "font-bold text-lg" (text ์ถฉ๋Œ โ†’ ๋’ค๊ฐ€ ์ด๊น€)

์ด์ œ Button ์ปดํฌ๋„ŒํŠธ์— ์ ์šฉํ•˜๋ฉด:

import { twMerge } from 'tailwind-merge';
 
// โœ… className ์˜ค๋ฒ„๋ผ์ด๋“œ๊ฐ€ ์•ˆ์ „ํ•˜๊ฒŒ ๋™์ž‘!
function Button({ className, children }: { className?: string; children: React.ReactNode }) {
  return (
    <button className={twMerge(
      'bg-blue-600 text-white px-6 py-3 rounded-xl font-semibold', // ๊ธฐ๋ณธ๊ฐ’
      className  // ๐Ÿฆ ์˜ํ˜ธ: "์ด ์ชฝ์ด ๋’ค์— ์˜ค๋‹ˆ๊นŒ, ๊ธฐ๋ณธ๊ฐ’์„ ๊น”๋”ํ•˜๊ฒŒ ์˜ค๋ฒ„๋ผ์ด๋“œํ•ด์š”."
    )}>
      {children}
    </button>
  );
}
 
// ์‚ฌ์šฉ
<Button className="px-3 py-1.5 text-sm">์ž‘์€ ๋ฒ„ํŠผ</Button>
// ๊ฒฐ๊ณผ: "bg-blue-600 text-white rounded-xl font-semibold px-3 py-1.5 text-sm"
// px-6 โ†’ px-3, py-3 โ†’ py-1.5 ๋กœ ์˜ค๋ฒ„๋ผ์ด๋“œ ์„ฑ๊ณต!

๐ŸŽจ ๋ณ€ํ˜•(Variant) ๊ธฐ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ: cva

cva (class-variance-authority) ๋Š” ์ปดํฌ๋„ŒํŠธ์˜ ๋‹ค์–‘ํ•œ ๋ณ€ํ˜•(variant, size ๋“ฑ)์„ ํƒ€์ž… ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ. tailwind-merge ๊ฐ€ "ํด๋ž˜์Šค ์ถฉ๋Œ์„ ํ•ด๊ฒฐ" ํ•œ๋‹ค๋ฉด, cva ๋Š” "๋ณ€ํ˜• ์กฐํ•ฉ์„ ๊ตฌ์กฐํ™”" ํ•ด.

์˜์ฒ ์ด๊ฐ€ tailwind-merge ๋ฅผ ๋ฐฐ์šด ๋’ค ์ด๋ ‡๊ฒŒ ๋งŒ๋“ค์—ˆ์–ด:

// ๐Ÿฃ ์˜์ฒ  2์•ˆ: twMerge ๋Š” ์“ฐ์ง€๋งŒ, ๋ณ€ํ˜• ์ฒ˜๋ฆฌ๊ฐ€ ๋˜ ์ง€์ €๋ถ„ํ•ด์ง
function Button({ variant, size, className, children }) {
  const variantClass = variant === 'secondary' ? 'bg-gray-100 text-gray-700'
                     : variant === 'danger'    ? 'bg-red-600 text-white'
                     : 'bg-blue-600 text-white'; // default: primary
  const sizeClass = size === 'sm' ? 'px-3 py-1.5 text-sm'
                  : size === 'lg' ? 'px-6 py-3 text-base'
                  : 'px-5 py-2.5 text-sm'; // default: md
  return (
    <button className={twMerge(variantClass, sizeClass, className)}>
      {children}
    </button>
  );
}

๐Ÿฆ ์˜ํ˜ธ: "variant ๊ฐ€ 5๊ฐœ, size ๊ฐ€ 4๊ฐœ ๋˜๋ฉด if-else ์ฒด์ธ์ด 20๊ฐœ๊ฐ€ ๋ผ์š”. TypeScript ์ž๋™์™„์„ฑ๋„ ์•ˆ ๋˜๊ณ ์š”. ์ด๊ฑธ cva ๋กœ ์„ ์–ธํ˜•์œผ๋กœ ์ •์˜ํ•˜๋ฉด ์ฝ”๋“œ๋„ ์ค„๊ณ  ํƒ€์ž…๋„ ์ƒ๊ฒจ์š”."

npm install class-variance-authority

๊ธฐ๋ณธ ๊ตฌ์กฐ

import { cva, type VariantProps } from 'class-variance-authority';
 
const buttonVariants = cva(
  // 1. ๊ธฐ๋ณธ ํด๋ž˜์Šค (ํ•ญ์ƒ ์ ์šฉ)
  'inline-flex items-center justify-center rounded-xl font-semibold transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
  {
    variants: {
      // 2. ๋ณ€ํ˜• ์ •์˜
      variant: {
        primary:   'bg-blue-600 text-white hover:bg-blue-700 focus-visible:ring-blue-500',
        secondary: 'bg-gray-100 text-gray-700 hover:bg-gray-200 focus-visible:ring-gray-400',
        danger:    'bg-red-600 text-white hover:bg-red-700 focus-visible:ring-red-500',
        ghost:     'text-gray-600 hover:bg-gray-100 focus-visible:ring-gray-400',
        link:      'text-blue-600 underline-offset-4 hover:underline focus-visible:ring-blue-500',
      },
      size: {
        sm:   'h-8 px-3 text-sm gap-1.5',
        md:   'h-10 px-5 text-sm gap-2',
        lg:   'h-12 px-6 text-base gap-2.5',
        icon: 'h-10 w-10 p-0',
      },
    },
    // 3. ๊ธฐ๋ณธ๊ฐ’
    defaultVariants: {
      variant: 'primary',
      size: 'md',
    },
  }
);

cva ๊ธฐ๋ฐ˜ Button ์ปดํฌ๋„ŒํŠธ

import { cva, type VariantProps } from 'class-variance-authority';
import { twMerge } from 'tailwind-merge';
 
const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-xl font-semibold transition-all duration-200 active:scale-95 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
  {
    variants: {
      variant: {
        primary:   'bg-blue-600 text-white hover:bg-blue-700 focus-visible:ring-blue-500',
        secondary: 'bg-gray-100 text-gray-700 hover:bg-gray-200 focus-visible:ring-gray-400',
        danger:    'bg-red-600 text-white hover:bg-red-700 focus-visible:ring-red-500',
        ghost:     'text-gray-600 hover:bg-gray-100 focus-visible:ring-gray-400',
      },
      size: {
        sm: 'h-8 px-3 text-xs gap-1.5',
        md: 'h-10 px-5 text-sm gap-2',
        lg: 'h-12 px-6 text-base gap-2.5',
      },
    },
    defaultVariants: {
      variant: 'primary',
      size: 'md',
    },
  }
);
 
// VariantProps ๋กœ ํƒ€์ž… ์ž๋™ ์ถ”๋ก !
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
  VariantProps<typeof buttonVariants> & {
    loading?: boolean;
  };
 
export function Button({ variant, size, loading, className, children, ...props }: ButtonProps) {
  return (
    <button
      disabled={loading || props.disabled}
      className={twMerge(buttonVariants({ variant, size }), className)}
      {...props}
    >
      {loading && (
        <span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
      )}
      {children}
    </button>
  );
}
 
// ์‚ฌ์šฉ ์˜ˆ โ€” ํƒ€์ž… ์ž๋™์™„์„ฑ์ด ๋จ!
<Button>๊ธฐ๋ณธ ๋ฒ„ํŠผ</Button>
<Button variant="secondary">๋ณด์กฐ ๋ฒ„ํŠผ</Button>
<Button variant="danger" size="lg">์‚ญ์ œ ๋ฒ„ํŠผ (ํฌ๊ฒŒ)</Button>
<Button variant="ghost" size="sm">ํ…์ŠคํŠธ ๋ฒ„ํŠผ (์ž‘๊ฒŒ)</Button>
<Button loading>์ œ์ถœ ์ค‘...</Button>
<Button className="w-full">๋„ˆ๋น„ ๊ฝ‰ ์ฑ„์šฐ๊ธฐ (twMerge ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ)</Button>

๐Ÿ”— cva + tailwind-merge ์กฐํ•ฉ (์ตœ๊ฐ• ํŒจํ„ด)

cn() ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ (์—…๊ณ„ ํ‘œ์ค€)

shadcn/ui ๋ฅผ ๋น„๋กฏํ•œ ๋Œ€๋ถ€๋ถ„์˜ ํ˜„๋Œ€ React ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ํŒจํ„ด์ด์•ผ.

// lib/utils.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
 
// cn = clsx + tailwind-merge
// clsx: ์กฐ๊ฑด๋ถ€ ํด๋ž˜์Šค ๋ณ‘ํ•ฉ (object/array ํ˜•ํƒœ๋„ ์ง€์›)
// twMerge: Tailwind ํด๋ž˜์Šค ์ถฉ๋Œ ํ•ด๊ฒฐ
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
npm install clsx tailwind-merge

cn() ์‚ฌ์šฉ ์˜ˆ

import { cn } from '@/lib/utils';
 
// ์กฐ๊ฑด๋ถ€ ํด๋ž˜์Šค + ์ถฉ๋Œ ํ•ด๊ฒฐ์„ ํ•œ ๋ฒˆ์—!
<div className={cn(
  'base-class',
  isActive && 'active-class',
  isDisabled && 'disabled-class',
  className  // ์˜ค๋ฒ„๋ผ์ด๋“œ ํด๋ž˜์Šค
)} />
 
// ์‹ค์ „: ํƒญ ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ
function Tab({ active, children, onClick }: Props) {
  return (
    <button
      onClick={onClick}
      className={cn(
        'border-b-2 px-4 py-2 text-sm font-medium transition-colors',
        active
          ? 'border-blue-600 text-blue-600'
          : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'
      )}
    >
      {children}
    </button>
  );
}

๐Ÿ’ป ์‹ค์ „: ์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

Badge ์ปดํฌ๋„ŒํŠธ

// components/Badge.tsx
const badgeVariants = cva(
  'inline-flex items-center rounded-full px-3 py-0.5 text-xs font-semibold',
  {
    variants: {
      variant: {
        default: 'bg-gray-100 text-gray-700',
        primary: 'bg-blue-100 text-blue-700',
        success: 'bg-green-100 text-green-700',
        warning: 'bg-amber-100 text-amber-700',
        danger:  'bg-red-100 text-red-700',
        purple:  'bg-purple-100 text-purple-700',
      },
    },
    defaultVariants: { variant: 'default' },
  }
);
 
export function Badge({ variant, className, children }: VariantProps<typeof badgeVariants> & { className?: string; children: React.ReactNode }) {
  return (
    <span className={cn(badgeVariants({ variant }), className)}>
      {children}
    </span>
  );
}
 
// ์‚ฌ์šฉ
<Badge>๊ธฐ๋ณธ</Badge>
<Badge variant="primary">React</Badge>
<Badge variant="success">๋ชจ์ง‘ ์ค‘</Badge>
<Badge variant="danger">๋งˆ๊ฐ</Badge>

Input ์ปดํฌ๋„ŒํŠธ

// components/Input.tsx
const inputVariants = cva(
  'block w-full rounded-lg border text-sm placeholder:text-gray-400 transition-colors focus:outline-none focus:ring-2',
  {
    variants: {
      variant: {
        default: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500/20',
        error:   'border-red-400 focus:border-red-500 focus:ring-red-500/20',
        success: 'border-green-400 focus:border-green-500 focus:ring-green-500/20',
      },
      size: {
        sm: 'px-3 py-2',
        md: 'px-4 py-2.5',
        lg: 'px-4 py-3 text-base',
      },
    },
    defaultVariants: { variant: 'default', size: 'md' },
  }
);
 
type InputProps = React.InputHTMLAttributes<HTMLInputElement> &
  VariantProps<typeof inputVariants> & {
    label?: string;
    error?: string;
    hint?: string;
  };
 
export function Input({ label, error, hint, variant, size, className, id, ...props }: InputProps) {
  const inputId = id ?? label?.toLowerCase().replace(/\s/g, '-');
  const resolvedVariant = error ? 'error' : variant;
 
  return (
    <div className="flex flex-col gap-1.5">
      {label && (
        <label htmlFor={inputId} className="text-sm font-medium text-gray-700">
          {label}
        </label>
      )}
      <input
        id={inputId}
        className={cn(inputVariants({ variant: resolvedVariant, size }), className)}
        {...props}
      />
      {error && <p className="text-xs text-red-500">{error}</p>}
      {!error && hint && <p className="text-xs text-gray-400">{hint}</p>}
    </div>
  );
}
 
// ์‚ฌ์šฉ
<Input label="์ด๋ฉ”์ผ" type="email" placeholder="you@example.com" />
<Input label="๋น„๋ฐ€๋ฒˆํ˜ธ" type="password" error="8์ž ์ด์ƒ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”" />
<Input label="๋‹‰๋„ค์ž„" hint="ํ•œ๊ธ€ ๋˜๋Š” ์˜๋ฌธ 2~20์ž" />

๐Ÿ ์ด๋ฒˆ์— ๋ฐฐ์šด ๋‚ด์šฉ ์ด์ •๋ฆฌ

๋„๊ตฌ์—ญํ• ์–ธ์ œ ์‚ฌ์šฉ
tailwind-mergeํด๋ž˜์Šค ์ถฉ๋Œ ํ•ด๊ฒฐclassName ์˜ค๋ฒ„๋ผ์ด๋“œ ์‹œ ํ•ญ์ƒ
cva๋ณ€ํ˜• ๊ธฐ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ์—ฌ๋Ÿฌ variant ๊ฐ€ ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ
clsx์กฐ๊ฑด๋ถ€ ํด๋ž˜์Šค ๋ณ‘ํ•ฉisActive && 'class' ํŒจํ„ด
cn()clsx + twMerge ์กฐํ•ฉ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ className ์ฒ˜๋ฆฌ

๐Ÿ“ ๋งˆ๋ฌด๋ฆฌ ํ€ด์ฆˆ

Q1. twMerge('p-4 bg-blue-500', 'p-6 bg-red-500') ์˜ ๊ฒฐ๊ณผ๋Š”?

โœ… ์ •๋‹ต: "bg-red-500 p-6" (๋˜๋Š” "p-6 bg-red-500" โ€” ์ˆœ์„œ๋Š” ๊ตฌํ˜„์— ๋”ฐ๋ฆ„)

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค:

  • tailwind-merge ๋Š” ๊ฐ™์€ CSS ์†์„ฑ์„ ๋Œ€์ƒ์œผ๋กœ ํ•˜๋Š” Tailwind ํด๋ž˜์Šค ์ค‘ ๋‚˜์ค‘์— ์˜ค๋Š” ๊ฐ’์ด ์ด๊ธฐ๋„๋ก ์ฒ˜๋ฆฌํ•ด.
  • p-4 ์™€ p-6 ์€ ๋ชจ๋‘ padding ์„ ๋Œ€์ƒ์œผ๋กœ ํ•˜๋ฏ€๋กœ ๋‚˜์ค‘ ๊ฐ’์ธ p-6 ๋งŒ ๋‚จ์•„.
  • bg-blue-500 ๊ณผ bg-red-500 ์€ ๋ชจ๋‘ background-color ๋ฅผ ๋Œ€์ƒ์œผ๋กœ ํ•˜๋ฏ€๋กœ ๋‚˜์ค‘ ๊ฐ’์ธ bg-red-500 ๋งŒ ๋‚จ์•„.
  • ์ด๊ฒŒ ๋ฐ”๋กœ className ์˜ค๋ฒ„๋ผ์ด๋“œ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ์›๋ฆฌ์•ผ.
  • ๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: "twMerge ๋Š” ๊ฐ™์€ CSS ์†์„ฑ ํด๋ž˜์Šค ์ถฉ๋Œ ์‹œ ๋‚˜์ค‘์ด ์ด๊ธด๋‹ค."

Q2. cva ์˜ defaultVariants ์„ค์ •์ด ์—†์œผ๋ฉด ์–ด๋–ค ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š”๊ฐ€?

โœ… ์ •๋‹ต: ๋ณ€ํ˜• props ๊ฐ€ ์ „๋‹ฌ๋˜์ง€ ์•Š์„ ๋•Œ ํ•ด๋‹น ๋ณ€ํ˜•์˜ ํด๋ž˜์Šค๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š์•„ ์Šคํƒ€์ผ์ด ์—†๋Š” ์ƒํƒœ๊ฐ€ ๋œ๋‹ค.

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค:

  • defaultVariants: { variant: 'primary', size: 'md' } ๊ฐ€ ์—†์œผ๋ฉด, <Button> ์ฒ˜๋Ÿผ ์•„๋ฌด props ๋„ ์ „๋‹ฌ ์•ˆ ํ•˜๋ฉด variant ํด๋ž˜์Šค๊ฐ€ ์—†์–ด์„œ ๋ฒ„ํŠผ์ด ํ…… ๋นˆ ์Šคํƒ€์ผ(๊ธฐ๋ณธ ๋ธŒ๋ผ์šฐ์ € ์Šคํƒ€์ผ)๋กœ ๋ณด์—ฌ.
  • VariantProps ํƒ€์ž…๋„ defaultVariants ๊ฐ€ ์žˆ์œผ๋ฉด ํ•ด๋‹น prop ์„ optional (variant?: ...) ๋กœ, ์—†์œผ๋ฉด required (variant: ...) ๋กœ ๋งŒ๋“ค์–ด์ค˜์„œ ํƒ€์ž… ์ˆ˜์ค€์—์„œ๋„ ์˜ํ–ฅ์ด ์žˆ์–ด.
  • ๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: "defaultVariants = ์•„๋ฌด๊ฒƒ๋„ ์•ˆ ์คฌ์„ ๋•Œ์˜ ๊ธฐ๋ณธ๊ฐ’. ์—†์œผ๋ฉด ์Šคํƒ€์ผ๋„ ์—†๋‹ค."

Q3. cn() ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜๊ฐ€ clsx ์™€ tailwind-merge ๋ฅผ ๋ชจ๋‘ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ ๋Š”?

โœ… ์ •๋‹ต: clsx ๋Š” ์กฐ๊ฑด๋ถ€/๋ฐฐ์—ด/๊ฐ์ฒด ํ˜•ํƒœ์˜ ํด๋ž˜์Šค๋ฅผ ๋ฌธ์ž์—ด๋กœ ํ•ฉ์ณ์ฃผ๊ณ , tailwind-merge ๋Š” ๊ทธ ๊ฒฐ๊ณผ์—์„œ Tailwind ํด๋ž˜์Šค ์ถฉ๋Œ์„ ํ•ด๊ฒฐํ•ด์ค€๋‹ค. ๋‘ ๋ฌธ์ œ๋ฅผ ๊ฐ์ž ์ž˜ ํ•ด๊ฒฐํ•˜๋Š” ๋„๊ตฌ๋ฅผ ์กฐํ•ฉํ•œ ๊ฒƒ์ด๋‹ค.

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค:

  • clsx('base', isActive && 'active', { 'special': isSpecial }) โ†’ "base active" (์กฐ๊ฑด๋ถ€ ์ฒ˜๋ฆฌ)
  • twMerge('px-4', 'px-6') โ†’ "px-6" (์ถฉ๋Œ ํ•ด๊ฒฐ)
  • cn = ๋จผ์ € clsx ๋กœ ์กฐ๊ฑด๋ถ€ ํด๋ž˜์Šค๋ฅผ ๋ฌธ์ž์—ด๋กœ ํ•ฉ์น˜๊ณ , ๊ทธ ๊ฒฐ๊ณผ๋ฅผ twMerge ์— ๋„˜๊ฒจ์„œ ์ถฉ๋Œ์„ ํ•ด๊ฒฐ.
  • clsx ๋งŒ ์“ฐ๋ฉด Tailwind ์ถฉ๋Œ์ด ํ•ด๊ฒฐ ์•ˆ ๋˜๊ณ , twMerge ๋งŒ ์“ฐ๋ฉด ์กฐ๊ฑด๋ถ€ ํด๋ž˜์Šค ์ฒ˜๋ฆฌ๊ฐ€ ๋ฒˆ๊ฑฐ๋กœ์›Œ.
  • ๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: "cn = clsx (์กฐ๊ฑด๋ถ€ ๋ณ‘ํ•ฉ) + twMerge (์ถฉ๋Œ ํ•ด๊ฒฐ). ๋‘ ๊ฐœ์˜ ์ง๊ฟ."

๐Ÿฃ ์˜์ฒ ์ด์˜ ํ‡ด๊ทผ ์ผ๊ธฐ

cn() ํ•จ์ˆ˜ ํ•˜๋‚˜ ๋งŒ๋“ค์–ด ๋‘๋ฉด ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์—์„œ ์“ธ ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒŒ ๋„ˆ๋ฌด ํŽธํ•˜๋‹ค. ์˜ํ˜ธ ๋‹˜์ด "์ด๊ฑด shadcn/ui ์—์„œ๋„ ์“ฐ๋Š” ํŒจํ„ด์ด์—์š”. ์—…๊ณ„ ํ‘œ์ค€์ด๋ผ ํŒ€์› ๋ˆ„๊ฐ€ ๋ด๋„ ๋ฐ”๋กœ ์ดํ•ดํ•ด์š”" ๋ผ๊ณ  ํ•˜์…จ๋Š”๋ฐ, ์ด๋Ÿฐ ๊ฒŒ ์ง„์งœ ์‹ค๋ฌด ๊ฐ๊ฐ์ด๊ตฌ๋‚˜.

cva ๋กœ Button ์ปดํฌ๋„ŒํŠธ ๋งŒ๋“ค ๋•Œ TypeScript ์ž๋™์™„์„ฑ์ด ์ฐฉ์ฐฉ ๋˜๋Š” ๊ฒŒ ์‹ ๊ธฐํ–ˆ๋‹ค. variant="danger" ๋ผ๊ณ  ํƒ€์ดํ•‘ํ•˜๋ฉด ์ž๋™์œผ๋กœ ์„ ํƒ์ง€๊ฐ€ ๋‚˜์˜ค๊ณ , ์—†๋Š” variant ์“ฐ๋ฉด ํƒ€์ž… ์—๋Ÿฌ๊ฐ€ ๋‚˜๊ณ . ์ด๊ฒŒ ํƒ€์ž… ์•ˆ์ „ํ•œ ์ปดํฌ๋„ŒํŠธ์˜ ์ง„์งœ ์˜๋ฏธ๊ตฌ๋‚˜ ์‹ถ์—ˆ๋‹ค.

๐Ÿ’ก ์˜ค๋Š˜์˜ ๊ตํ›ˆ: "์žฌ์‚ฌ์šฉ ์ปดํฌ๋„ŒํŠธ์˜ ํ•ต์‹ฌ์€ '์—ด๋ฆฐ ํ™•์žฅ, ๋‹ซํžŒ ์ˆ˜์ •'. cva ๋กœ variant ๋ฅผ ์ž˜ ์„ค๊ณ„ํ•˜๋ฉด ์ƒˆ ์Šคํƒ€์ผ์ด ํ•„์š”ํ•ด๋„ ๊ธฐ์กด ์ฝ”๋“œ๋ฅผ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š์•„๋„ ๋œ๋‹ค."

์ด๋ฒˆ ์ฃผ ์•ˆ์— ์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํŒŒ์ผ ํ•˜๋‚˜์— Button, Badge, Input ๋‹ค ๋ชจ์•„์„œ ์˜ํ˜ธ ๋‹˜ํ•œํ…Œ PR ์˜ฌ๋ ค๋ด์•ผ๊ฒ ๋‹ค. ๋‚ด์ผ ์•„์นจ ์ผ์ฐ ์ถœ๊ทผํ•ด์„œ ์‹œ์ž‘ํ•˜์ž. ์˜ค๋Š˜ ์ €๋…์€ ์ผ์ฐ ์ž์•ผ์ง€! ๐Ÿ’ค


๐Ÿ”— ๋” ์•Œ์•„๋ณด๊ธฐ