Skip to content

๐Ÿชต 6. ํšจ์œจ์ ์ธ UI ์ปดํฌ๋„ŒํŠธ ์Šคํƒ€์ผ๋ง: Tailwind CSS cn.ts

Taeyeon Yoon edited this page Dec 5, 2024 · 1 revision

ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์—์„œ๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ UI๋ฅผ ๋ถ„๋ฆฌํ•ด ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ณ  ์œ ์—ฐํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์„ค๊ณ„ํ•˜๋Š” ๋™์‹œ์— UI ๋ถ€๋ถ„์—์„œ์˜ ์ผ๊ด€๋œ ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ํšจ์œจ์ ์œผ๋กœ ๊ตฌํ˜„ํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ํ•ต์‹ฌ ๊ณผ์ œ๊ฐ€ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ข‹์€ ์ปดํฌ๋„ŒํŠธ๋ž€ ๋ฌด์—‡์ธ๊ฐ€? + Headless Pattern์—์„œ ์‚ดํŽด๋ณธ ๊ฒƒ์ฒ˜๋Ÿผ Headless ํŒจํ„ด์„ ๋”ฐ๋ฅด๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋”๋Ÿฌ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋Ÿฐ ๊ฒฝ์šฐ๋Š” ๋™์ ์ธ UI๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด Emotion ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๊ฒŒ ์ผ๋ฐ˜์ ์ด์ง€๋งŒ, ์šฐ๋ฆฌ๋Š” ๋น ๋ฅธ ํผ๋ธ”๋ฆฌ์‹ฑ ๊ตฌ์ถ•์„ ์œ„ํ•ด Tailwind CSS๋ฅผ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.

Tailwind CSS๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ ์ „์šฉ ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” CSS ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ์„œ Zero Runtime + ๋ฌธ์ž์—ด ๊ธฐ๋ฐ˜์˜ ํด๋ž˜์Šค ์กฐ์ž‘ ๊ธฐ๋ฐ˜์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—ฌ์„œ ์„ฑ๋Šฅ์ด ๊ฐ•๋ ฅํ•˜๋ฉด์„œ ํŽธํ•˜๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ํ…Œ์ผ์œˆ๋“œ ํŠน์„ฑ์ƒ ๋ถ„๋ฆฌ ๊ฐ€๋Šฅํ•˜๊ณ , ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ UI ์ปดํฌ๋„ŒํŠธ์— ์Šคํƒ€์ผ์„ ์ ์šฉํ•  ๋•Œ, ์•„๋ž˜์™€ ๊ฐ™์€ ์• ๋กœ์‚ฌํ•ญ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

  1. ์กฐ๊ฑด๋ถ€ ํด๋ž˜์Šค ์ ์šฉ์˜ ์–ด๋ ค์›€
  2. ํด๋ž˜์Šค ์ถฉ๋Œ ๋ฌธ์ œ
  3. ํด๋ž˜์Šค๋ช… ๋ณ‘ํ•ฉ์˜ ๋ณต์žก์„ฑ

์ฆ‰, ํ…Œ์ผ์œˆ๋“œ๋Š” ๋™์ ์œผ๋กœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค.(๋ฌผ๋ก  ๋ณต์žกํ•˜๊ฒŒ join, filter ๋“ฑ์„ ์“ฐ๋ฉด ๊ฐ€๋Šฅํ•˜์ง€๋งŒ์š”.)

์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋“ค์„ ํ•ด๊ฒฐํ•˜๊ณ  ๋” ํšจ์œจ์ ์ธ UI ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ์„ ์œ„ํ•ด, ์šฐ๋ฆฌ๋Š” ๋ช‡ ๊ฐ€์ง€ ํ•ต์‹ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์„ ์กฐํ•ฉํ•œ cn.ts ์œ ํ‹ธ๋ฆฌํ‹ฐ๋ฅผ ๊ตฌ์ถ•ํ–ˆ์Šต๋‹ˆ๋‹ค.

1. clsx

์กฐ๊ฑด๋ถ€ ํด๋ž˜์Šค๋ฅผ ์ง๊ด€์ ์œผ๋กœ ๊ฒฐํ•ฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ์ž‘์€ ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

clsx('base-class', isActive && 'active-class')
// -> isActive๊ฐ€ true๋ฉด 'base-class active-class'
// -> false๋ฉด 'base-class'

2. tailwind-merge

Tailwind ํด๋ž˜์Šค ๊ฐ„์˜ ์ถฉ๋Œ์„ CSS์Šค๋Ÿฝ๊ฒŒ ํ•ด๊ฒฐํ•ด์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

twMerge('px-2 py-1', 'p-3') // -> 'p-3'
twMerge('text-red-500', 'text-blue-500') // -> 'text-blue-500'

3. class-variance-authority (cva)

์ปดํฌ๋„ŒํŠธ์˜ variant๋ณ„ ํด๋ž˜์Šค๋ฅผ ํƒ€์ž… ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

const buttonVariants = cva('base-button-styles', {
  variants: {
    variant: {
      primary: 'bg-blue-500',
      secondary: 'bg-gray-500'
    },
    size: {
      sm: 'text-sm',
      lg: 'text-lg'
    }
  }
});

์ด ์„ธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ cn.ts ์œ ํ‹ธ๋ฆฌํ‹ฐ๋กœ ํ†ตํ•ฉํ•ด ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/utils/cn';

const buttonVariants = cva(
  'inline-flex items-center justify-center gap-2 whitespace-nowrap ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
  {
    variants: {
      variant: {
        proimary: 'bg-violet-500 border-2 border-violet-950 hover:bg-violet-600',
        secondary: 'bg-eastbay-900 border-2 border-violet-950 hover:bg-eastbay-950',
      },
      size: {
        sm: 'h-11 w-full text-2xl font-medium text-stroke-md',
        lg: 'h-14 rounded-2xl w-full text-2xl font-medium text-stroke-md',
        icon: 'h-10 w-10',
      },
    },
    defaultVariants: {
      variant: 'proimary',
      size: 'lg',
    },
  },
);

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, ...props }, ref) => {
  return <button className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
});
Button.displayName = 'Button';

export { Button, buttonVariants };

์ด๋Ÿฐ ์‹์œผ๋กœ ์‚ฌ์šฉํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

๊ฒฐ๋ก  ๋ฐ ์ด์ 

cn.ts ์œ ํ‹ธ๋ฆฌํ‹ฐ๋ฅผ ํ™œ์šฉํ•œ UI ์ปดํฌ๋„ŒํŠธ ์Šคํƒ€์ผ๋ง ์‹œ์Šคํ…œ์€ ์•„๋ž˜์™€ ๊ฐ™์€ ํ•ต์‹ฌ์ ์ธ ์ด์ ์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  1. ์ฒด๊ณ„์ ์ธ ๋””์ž์ธ ์‹œ์Šคํ…œ ๊ตฌํ˜„
    • ์ผ๊ด€๋œ ํด๋ž˜์Šค ์ฒ˜๋ฆฌ ํŒจํ„ด์œผ๋กœ ์ปดํฌ๋„ŒํŠธ ์Šคํƒ€์ผ๋ง
    • ํ”„๋กœ์ ํŠธ ์ „๋ฐ˜์˜ ๋””์ž์ธ ์ผ๊ด€์„ฑ ํ™•๋ณด
    • ์ฝ”๋“œ ์Šคํƒ€์ผ์˜ ๊ทœ๊ฒฉํ™” ๋‹ฌ์„ฑ
  2. ์ž๋™ํ™”๋œ ์Šคํƒ€์ผ ๊ด€๋ฆฌ
    • ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€/์™ธ๋ถ€ ์Šคํƒ€์ผ ์šฐ์„ ์ˆœ์œ„ ์ž๋™ ์กฐ์ •
    • ํด๋ž˜์Šค ์ถฉ๋Œ ์—†๋Š” ์•ˆ์ „ํ•œ ์Šคํƒ€์ผ ํ™•์žฅ
    • ์กฐ๊ฑด๋ถ€ ์Šคํƒ€์ผ๋ง์˜ ๊ฐ„์†Œํ™”
  3. ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ ํ–ฅ์ƒ
    • ํƒ€์ž… ์‹œ์Šคํ…œ์„ ํ†ตํ•œ props ์ž๋™ ์™„์„ฑ
    • ์ปดํฌ๋„ŒํŠธ variants์™€ size ๋“ฑ์˜ ํƒ€์ž… ์•ˆ์ „์„ฑ
    • ๋นŒ๋“œ ํƒ€์ž„ ์ตœ์ ํ™”๋กœ ๋Ÿฐํƒ€์ž„ ์„ฑ๋Šฅ ๋ณด์žฅ
  4. ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ ์•„ํ‚คํ…์ฒ˜
    • ์Šคํƒ€์ผ ๋ณ€ํ˜•์˜ ์žฌ์‚ฌ์šฉ์„ฑ ํ™•๋ณด
    • ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ์˜ ์œ ์—ฐํ•œ ํ™•์žฅ
    • ์ƒˆ๋กœ์šด ๋””์ž์ธ ์š”๊ตฌ์‚ฌํ•ญ์— ๋น ๋ฅธ ๋Œ€์‘

์ด๋ ‡๊ฒŒ ๋งŒ๋“  Tailwind CSS ๊ธฐ๋ฐ˜์˜ UI ์ปดํฌ๋„ŒํŠธ ์‹œ์Šคํ…œ์€ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋ฉด์„œ๋„ ์œ ์—ฐํ•œ ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ํšจ์œจ์ ์œผ๋กœ ๊ตฌํ˜„ํ•˜๊ณ  ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค.

๐Ÿ˜Ž ์›จ๋ฒ ๋ฒ ๋ฒ ๋ฒฑ

๐Ÿ‘ฎ๐Ÿป ํŒ€ ๊ทœ์น™

๐Ÿ’ป ํ”„๋กœ์ ํŠธ

๐Ÿชต ์›จ๋ฒ ๋ฒฑ ๊ธฐ์ˆ ๋กœ๊ทธ

๐Ÿช„ ๋ฐ๋ชจ ๊ณต์œ 

๐Ÿ”„ ์Šคํ”„๋ฆฐํŠธ ๊ธฐ๋ก

๐Ÿ“— ํšŒ์˜๋ก

Clone this wiki locally