๐งฉ Tailwind 11์ฅ: ์ปดํฌ๋ํธ ํจํด๊ณผ ์ฌ์ฌ์ฉ ์ค๊ณ
๐ ๊ฐ์
cva, tailwind-merge ๋ก ํ์ ์์ ํ ์ฌ์ฌ์ฉ ์ปดํฌ๋ํธ ๋ง๋ค๊ธฐ โ ๋ณํ(variant) ๊ธฐ๋ฐ ์ปดํฌ๋ํธ ์ค๊ณ์ ์ ์
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ์์์ผ ํ๋๊ฐ
- โ ๏ธ ํด๋์ค ์ถฉ๋ ๋ฌธ์ : tailwind-merge
- ๐จ ๋ณํ(Variant) ๊ธฐ๋ฐ ์ปดํฌ๋ํธ: cva
- ๐ cva + tailwind-merge ์กฐํฉ (์ต๊ฐ ํจํด)
- ๐ป ์ค์ : ์์๋ค ์ปค๋ฎค๋ํฐ ์ปดํฌ๋ํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- ๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 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-mergecn() ์ฌ์ฉ ์
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 ์ฌ๋ ค๋ด์ผ๊ฒ ๋ค. ๋ด์ผ ์์นจ ์ผ์ฐ ์ถ๊ทผํด์ ์์ํ์. ์ค๋ ์ ๋ ์ ์ผ์ฐ ์์ผ์ง! ๐ค