Button ์ปดํฌ๋„ŒํŠธ ๋ฆฌํŒฉํ† ๋ง ํ•˜๊ธฐ

2025. 10. 27. 20:28ใ†Trouble Shooting & Issues/Linkiving

๋ฐ˜์‘ํ˜•

๋ชฉ์ฐจ

1. ์ด์ „ ์ฝ”๋“œ

2. ๋ฆฌํŒฉํ† ๋ง

3. Trouble Shooting


1. ์ด์ „ ์ฝ”๋“œ

์šฐ์„  ์ด์ „ ์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด์ž.

Button ์ปดํฌ๋„ŒํŠธ๋Š” ๊ธฐ๋ณธ ์ปดํฌ๋„ŒํŠธ ์ค‘์˜ ๊ธฐ๋ณธ ๋А๋‚Œ์ด ์žˆ์–ด์„œ ๊ฐ€์žฅ ์ฒ˜์Œ ์ž‘์„ฑํ•œ ์ฝ”๋“œ์ด๊ณ , ๊ทธ๋งŒํผ ์—‰๋ง์ง„์ฐฝ์ด๋‹ค.

์ด ์ปดํฌ๋„ŒํŠธ๋Š” text ๋ฒ„ํŠผ, text+icon ๋ฒ„ํŠผ์ด๋‹ค. (ํ…์ŠคํŠธ๊ฐ€ ๋ฌด์กฐ๊ฑด ๋“ค์–ด๊ฐ€ ์žˆ์–ด์•ผ ํ•œ๋‹ค๋Š” ๋œป)

 

์•„๋ž˜์™€ ๊ฐ™์ด ์ชผ๊ฐœ์„œ ์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด์ž.

  • interface ButtonProps { ... }
  • function Button { ... }
    • Props ์ •์˜๋ถ€
    • ์Šคํƒ€์ผ๋ง ์ •์˜๋ถ€
    • ๋ฒ„ํŠผ ์ถœ๋ ฅ๋ถ€

interface ์ •์˜

'use client';

import { clsx } from 'clsx';
import React from 'react';

interface ButtonProps {
  children: React.ReactNode;	// text
  onClick?: React.MouseEventHandler<HTMLButtonElement>;
  type?: 'button' | 'submit' | 'reset';
  variant?: 'primary' | 'outline' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  disabled?: boolean;
  icon?: React.ReactNode;	// ๋“ค์–ด๊ฐˆ ์•„์ด์ฝ˜
  iconPosition?: 'left' | 'right';	// ์•„์ด์ฝ˜ ์œ„์น˜
}

ํฌ๊ฒŒ ์„ค๋ช…ํ•  ๋ถ€๋ถ„์ด ์—†๋‹ค. ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋‹ค๋ฃฐ ๋•Œ ํ•„์š”ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•œ props๋“ค์„ ์ ์—ˆ๋‹ค.

children, icon์ด ๋ชจ๋‘ React.ReactNode์ธ ์ด์œ ๋Š” ์ด ์ž๋ฆฌ์— ์–ด๋–ค ๊ฒƒ์ด ๋“ค์–ด๊ฐˆ ์ง€ ์•„์ง ๊ตฌ์ฒด์ ์œผ๋กœ ์ƒ๊ฐํ•˜์ง€ ๋ชปํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

 

props ์ •์˜๋ถ€

export default function Button({
  children,
  variant = 'primary',
  onClick,
  type = 'button',
  size = 'md',
  icon,
  iconPosition = 'left',
  disabled,
}: ButtonProps) { ...

Button์„ const Button์ด ์•„๋‹Œ export default function์˜ ํ˜•์‹์œผ๋กœ ์ •์˜ํ•˜๊ณ  ์žˆ๋‹ค.

default๊ฐ’์ด ํ•„์š”ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•˜๋Š” ๋ถ€๋ถ„๋“ค์— default ๊ฐ’์„ ์ง€์ •ํ•ด์คฌ์œผ๋ฉฐ, ButtonProps๋ฅผ ํƒ€์ž…์œผ๋กœ ์ง€์ •ํ–ˆ๋‹ค.

 

์Šคํƒ€์ผ๋ง ์ •์˜๋ถ€

{
  const baseStyle = 'inline-flex items-center outline-none rounded font-medium transition';
  const variants = {
    primary: 'bg-blue-500 text-white hover:bg-gray-800 cursor-pointer',
    outline: 'border border-gray-300 text-gray-700 hover:bg-gray-100 cursor-pointer',
    ghost: 'text-gray-700 hover:bg-gray-100 cursor-pointer',
  };
  const sizes = {
    sm: 'px-3 py-1.5 text-sm',
    md: 'px-4 py-2 text-base',
    lg: 'px-5 py-3 text-lg',
  };

  // ํด๋ž˜์Šค ์กฐํ•ฉ๋ถ€
  const classes = clsx(baseStyle, variants[variant], sizes[size], {
    'cursor-not-allowed bg-gray-400 opacity-50': disabled,
    'hover:cursor-not-allowed hover:bg-gray-400 hover:opacity-50': disabled,
  });

tailwind๋กœ ์Šคํƒ€์ผ์„ ์ •์˜ํ•ด์คฌ๋‹ค.

baseStyle๋กœ ๊ธฐ๋ณธ ๋ฒ„ํŠผ ์Šคํƒ€์ผ์„ ์ž‘์„ฑํ–ˆ์œผ๋ฉฐ, variants, sizes ๊ฐ’์— ๋”ฐ๋ฅธ ์Šคํƒ€์ผ๋„ ์ž‘์„ฑํ–ˆ๋‹ค.

classes๋Š” ์œ„์—์„œ ์ž‘์„ฑํ•œ ์Šคํƒ€์ผ๋“ค์„ clsx๋กœ ํ•ฉ์นœ๋‹ค.

 

๋ฒ„ํŠผ ์ถœ๋ ฅ๋ถ€

  // ๋ฒ„ํŠผ ์‹ค์ œ ์ถœ๋ ฅ ๋ถ€๋ถ„
  const ButtonContent = (
    <>
      {icon && iconPosition === 'left' && <span className="mr-2">{icon}</span>}
      {children}
      {icon && iconPosition === 'right' && <span className="ml-2">{icon}</span>}
    </>
  );

  return (
    <button
      type={type}
      className={classes}
      disabled={disabled}
      aria-disabled={disabled}
      onClick={onClick}
    >
      {ButtonContent}
    </button>
  );
}

๋ฒ„ํŠผ์„ ์‹ค์ œ๋กœ ์ถœ๋ ฅํ•˜๋Š” ๋ถ€๋ถ„์ด๋‹ค.

ButtonContent๋ฅผ ๋”ฐ๋กœ ์ž‘์„ฑํ•˜์—ฌ return์—์„œ๋Š” {ButtonContent}๋งŒ ์ž…๋ ฅํ•˜๋„๋ก ํ–ˆ๋‹ค.

 

 

์ „์ฒด ์ฝ”๋“œ

'use client';

import { clsx } from 'clsx';
import React from 'react';

interface ButtonProps {
  children: React.ReactNode;
  onClick?: React.MouseEventHandler<HTMLButtonElement>;
  type?: 'button' | 'submit' | 'reset';
  variant?: 'primary' | 'outline' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  disabled?: boolean;
  icon?: React.ReactNode;
  iconPosition?: 'left' | 'right';
}

export default function Button({
  children,
  variant = 'primary',
  onClick,
  type = 'button',
  size = 'md',
  icon,
  iconPosition = 'left',
  disabled,
}: ButtonProps) {
  const baseStyle = 'inline-flex items-center outline-none rounded font-medium transition';
  const variants = {
    primary: 'bg-blue-500 text-white hover:bg-gray-800 cursor-pointer',
    outline: 'border border-gray-300 text-gray-700 hover:bg-gray-100 cursor-pointer',
    ghost: 'text-gray-700 hover:bg-gray-100 cursor-pointer',
  };
  const sizes = {
    sm: 'px-3 py-1.5 text-sm',
    md: 'px-4 py-2 text-base',
    lg: 'px-5 py-3 text-lg',
  };

  // ํด๋ž˜์Šค ์กฐํ•ฉ๋ถ€
  const classes = clsx(baseStyle, variants[variant], sizes[size], {
    'cursor-not-allowed bg-gray-400 opacity-50': disabled,
    'hover:cursor-not-allowed hover:bg-gray-400 hover:opacity-50': disabled,
  });

  // ๋ฒ„ํŠผ ์‹ค์ œ ์ถœ๋ ฅ ๋ถ€๋ถ„
  const ButtonContent = (
    <>
      {icon && iconPosition === 'left' && <span className="mr-2">{icon}</span>}
      {children}
      {icon && iconPosition === 'right' && <span className="ml-2">{icon}</span>}
    </>
  );

  return (
    <button
      type={type}
      className={classes}
      disabled={disabled}
      aria-disabled={disabled}
      onClick={onClick}
    >
      {ButtonContent}
    </button>
  );
}

 

 


2. ๋ฆฌํŒฉํ† ๋ง

0. ๋ฌธ์ œ์  ์งš๊ธฐ

๊ทธ๋Ÿผ ์ด์ œ ์ด์ „ ์ฝ”๋“œ์—์„œ ์ƒ๊ฐํ•œ ๋ฌธ์ œ์ ๋“ค, ๊ณ ์น˜๊ณ  ์‹ถ์€ ๋ถ€๋ถ„๋“ค์„ ์ •๋ฆฌํ•ด๋ณด์ž.

 

1. ํ™•์žฅ์„ฑ์˜ ๋ถ€์žฌ

ํ˜„์žฌ ButtonProps๋Š” ๋‚ด๊ฐ€ ์ •์˜ํ•œ props ์™ธ์˜ ๊ฒƒ์„ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์—†๋„๋ก ์ž‘์„ฑ๋˜์–ด ์žˆ๋‹ค.

Button์€ HTML์˜ ๊ธฐ๋ณธ ์š”์†Œ๋กœ, HTMLButtonElement์˜ ์†์„ฑ์„ ์ƒ์†๋ฐ›์•„ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ•ด์•ผ ํ•œ๋‹ค.

 

2. tailwind ์ •๋ ฌ์ด ์•ˆ๋จ

์ž‘์€ ๋ถ€๋ถ„์ผ ์ˆ˜ ์žˆ์œผ๋‚˜, ๋‹ค๋ฅธ ๋ฏธ๋‹ˆ ํ”„๋กœ์ ํŠธ์—์„œ tailwind ์ •๋ ฌ์„ ํ–ˆ๋Š”๋ฐ ์ƒ๋‹นํžˆ ํŽธํ–ˆ๋‹ค.

๊ทธ๋ž˜์„œ tailwind ์ •๋ ฌ์„ ์ ์šฉํ•˜๊ณ  ์‹ถ์—ˆ๋‹ค.

 

3. props ์ •๋ ฌ ์•ˆ๋จ

tailiwnd ์ •๋ ฌ์„ ์ƒ๊ฐํ•˜๋‹ˆ๊นŒ props ์ •๋ ฌ์ด ์•ˆ๋œ ๊ฒƒ๋„ ์‹ ๊ฒฝ์ด ์“ฐ์—ฌ ์ด ๋ถ€๋ถ„๋„ ํ•ด๊ฒฐํ•˜๊ณ  ์‹ถ์—ˆ๋‹ค.

 

4. design์ด ๋‚˜์˜ด์— ๋”ฐ๋ผ ๋ฐœ๊ฒฌ๋œ ์ขŒ์šฐ padding ๋ฌธ์ œ

๋‹น์‹œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ๋Š” ๋””์ž์ธ์ด ์ „ํ˜€ ๋‚˜์˜ค์ง€ ์•Š์•˜๋Š”๋ฐ, ์ง€๊ธˆ์€ ๋””์ž์ธ์ด ์–ด๋А์ •๋„ ๋‚˜์˜จ ์ƒํƒœ๋‹ค.

figma๋ฅผ ํ™•์ธํ•˜๋‹ˆ๊นŒ ์•„์ด์ฝ˜์ด ์žˆ๋Š” ์ชฝ๊ณผ ํ…์ŠคํŠธ๊ฐ€ ์žˆ๋Š” ์ชฝ์˜ ํŒจ๋”ฉ๊ฐ’์ด ๋‹ฌ๋ž๋Š”๋ฐ, ์ฐฉ์‹œ๋ฅผ ๊ณ ๋ คํ•˜์—ฌ ์˜๋„๋œ ๋””์ž์ธ์ด๋ผ๊ณ  ํ–ˆ๋‹ค.

 

5. ํ•จ์ˆ˜ ์„ ์–ธ์‹ vs ํ•จ์ˆ˜ ํ‘œํ˜„์‹

๊ธฐ์กด Button์€ export default function Button(...)์œผ๋กœ ํ•จ์ˆ˜ ์„ ์–ธ์‹์œผ๋กœ ์ž‘์„ฑ๋˜์—ˆ๋‹ค.

์ผ๊ด€๋œ ์„ ์–ธ ์Šคํƒ€์ผ๊ณผ ๋ฆฌํŒฉํ† ๋ง ์œ ์—ฐ์„ฑ ๋“ฑ์„ ์œ„ํ•ด ํ•จ์ˆ˜ ํ‘œํ˜„์‹์œผ๋กœ ์ˆ˜์ •ํ•˜๋„๋ก ํ•œ๋‹ค.

 

 

1. ํ™•์žฅ์„ฑ

export interface ButtonProps
  extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>,
  'disabled' | 'onClick' | 'children'> {
  ...
}

ButtonProps๊ฐ€ HTMLButtonElement์˜ ์†์„ฑ์„ ์ƒ์† ๋ฐ›๋„๋ก ์ˆ˜์ •ํ–ˆ๋‹ค.

 

โœ… React.ButtonHTMLAttributes<HTMLButtonElement>

React๊ฐ€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๊ณตํ•˜๋Š” ๋ฒ„ํŠผ ์š”์†Œ์˜ ๋ชจ๋“  ์†์„ฑ ํƒ€์ž…์ด๋‹ค.

์ด ์†์„ฑ๋“ค์„ extends ํ•ด ์˜ด์œผ๋กœ์จ ๋ฒ„ํŠผ์˜ ๊ธฐ๋ณธ ์†์„ฑ์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

<Button disabled onClick={handleClick}>๋ฒ„ํŠผ</Button>

 

โœ… Omit<..., 'disabled' | 'onClick' | 'children'>

Omit<T, K>๋Š” TypeScript์˜ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํƒ€์ž…์ด๋‹ค.

→ "ํƒ€์ž… T์—์„œ ํŠน์ • ํ‚ค K๋ฅผ ์ œ์™ธํ•œ ๋‚˜๋จธ์ง€ ์†์„ฑ๋งŒ ๋‚จ๊ฒจ๋ผ"

์ปค์Šคํ…€ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ƒˆ๋กœ ์ •์˜ํ•˜๋ ค๋Š” ์†์„ฑ์„ ์ œ์™ธ์‹œํ‚ค๊ธฐ ์œ„ํ•ด์„œ ์‚ฌ์šฉํ•œ๋‹ค.

์ด ์ฝ”๋“œ์—์„œ๋Š” disabled, onClick, children์— ๋Œ€ํ•ด ์žฌ์ •์˜๋ฅผ ํ•˜๊ณ  ์žˆ๋‹ค.

 

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(
  {
    ...,
    ...rest
  },
  ref
) { ...

โœ… React.forwardRef<HTMLButtonElement, ButtonProps>

React.forwardRef๋Š” ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ref๋ฅผ ์ž์‹ ๋‚ด๋ถ€์˜ ์‹ค์ œ DOM ์š”์†Œ๋‚˜ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ๋กœ ์ „๋‹ฌํ•˜๋Š” ํ•จ์ˆ˜์ด๋‹ค.

์ผ๋ฐ˜์ ์œผ๋กœ ํ•จ์ˆ˜ํ˜• ์ปดํฌ๋„ŒํŠธ๋Š” ref๋ฅผ ์ง์ ‘ ๋ฐ›์„ ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— React.forward๋กœ ๊ฐ์‹ธ์•ผ ref ์ „๋‹ฌ์ด ๊ฐ€๋Šฅํ•ด์ง„๋‹ค.

 

์ด ๋•Œ React.forwardRef() ์•ˆ์— ์ต๋ช… ํ•จ์ˆ˜๊ฐ€ ์•„๋‹Œ ๋ช…๋ช…๋œ ํ•จ์ˆ˜ function Button์„ ์‚ฌ์šฉํ•˜๋Š”๋ฐ,

๋””๋ฒ„๊น… ํŽธ์˜์„ฑ์„ ์œ„ํ•จ์ด๋‹ค.

๋งŒ์•ฝ ์ด ์ปดํฌ๋„ŒํŠธ์—์„œ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์ต๋ช…ํ•จ์ˆ˜๋ผ๋ฉด DevTools์ด๋‚˜ ์—๋Ÿฌ ์Šคํƒ์— <ForwardRef>๋กœ๋งŒ ๋ณด์ด๋Š”๋ฐ,

๋ช…๋ช…๋œ ํ•จ์ˆ˜ ํ˜•ํƒœ๋กœ ์ž‘์„ฑํ•˜๋ฉด Button์ด๋ผ๋Š” ์ด๋ฆ„์ด ๋ณด์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

 

2. tailwind ์ •๋ ฌ

tailwind ์ •๋ ฌ์„ ์œ„ํ•ด์„œ prettier-plugin-tailwindcss ๋ฐ tv๋ฅผ ์„ค์น˜ํ–ˆ๋‹ค.

import { tv } from 'tailwind-variants';

const styles = tv({
  base: 'inline-flex items-center justify-center rounded font-medium whitespace-nowrap transition-all duration-150 outline-none',
  variants: {
    variant: {
      primary: 'cursor-pointer bg-blue-500 text-white hover:bg-blue-600 active:bg-blue-700',
      outline:
        'cursor-pointer border border-gray-300 text-gray-700 hover:bg-gray-50 active:bg-gray-100',
      ghost: 'cursor-pointer bg-transparent text-gray-700 hover:bg-gray-100 active:bg-gray-200',
    },
    size: {
      sm: 'h-8 px-4 py-1.5 text-sm',
      md: 'h-10 px-5 py-2 text-base',
      lg: 'h-12 px-6 py-3 text-lg',
    },
    disabled: {
      true: 'pointer-events-none cursor-not-allowed bg-gray-400 opacity-50',
      false: '',
    },
  },
});

tv๋กœ ๊ฐ์‹ธ๋ฉด ์ •๋ ฌ์ด ๋œ๋‹ค.

base, variants ๋‚ด๋ถ€์— tailwind๋ฅผ ์ž‘์„ฑํ•ด์ค€๋‹ค.

์ด ๋•Œ disabled๋ฅผ ์ถ”๊ฐ€๋กœ ๋„˜๊ฒจ์ฃผ์—ˆ๋‹ค.

radius๋Š” ์‹ค์ œ๋กœ ์ ์šฉ์ด ๋  ์ง€ ์•„์ง ์•Œ ์ˆ˜ ์—†์–ด์„œ ์šฐ์„  ๋”ฐ๋กœ ์ž‘์„ฑํ•˜์ง€ ์•Š์•˜๋‹ค.

 

// STYLES
  const classes = styles({
    variant,
    size,
    disabled,
  });

์ •์˜ํ•œ ์Šคํƒ€์ผ์€ Button function ๋‚ด๋ถ€์—์„œ ํ•œ ๋ฒˆ๋” ์–ธ๊ธ‰ํ•˜์—ฌ props๋กœ ๋ฐ›์€ variant, size, disabled๋ฅผ ํ•ด๋‹น ๊ฐ’์œผ๋กœ ๋„˜๊ฒจ์ฃผ๋ฉด ๋œ๋‹ค.

 

 

3. props ์ •๋ ฌ

props ์ •๋ ฌ์„ ํ•˜๋ ค๊ณ  ๊ดœ์ฐฎ์€ ๋„๊ตฌ๊ฐ€ ์žˆ๋Š”์ง€ ์ฐพ์•„๋ดค๋Š”๋ฐ eslint-plugin-react์˜ jsx-sort-prop๊ฐ€ ์žˆ์—ˆ๋‹ค.

.eslintrc์— ํ•ด๋‹น ๊ทœ์น™์„ ์ž‘์„ฑํ•˜์—ฌ ์‚ฌ์šฉํ•ด๋ดค๋Š”๋ฐ, ๋ง‰์ƒ ์ •๋ ฌ์„ ์‚ฌ์šฉํ•ด๋ณด๋‹ˆ ์˜คํžˆ๋ ค ๋” ๋ถˆํŽธํ•ด์กŒ๋‹ค.

export interface ButtonProps
  extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'disabled' | 'onClick' | 'children'> {
  className?: string;
  disabled?: boolean;
  icon?: IconMapTypes;
  iconPosition?: 'left' | 'right';
  label?: string;
  size?: 'sm' | 'md' | 'lg';
  variant?: 'primary' | 'outline' | 'ghost';
  onClick?: React.MouseEventHandler<HTMLButtonElement>;
}

์ฝœ๋ฐฑ props๋ฅผ ๋งˆ์ง€๋ง‰์— ์ž‘์„ฑํ•˜๊ณ , ์˜ˆ์•ฝ์–ด๋ฅผ ๊ฐ€์žฅ ๋จผ์ € ์ž‘์„ฑํ•˜๋„๋ก ์„ธํŒ…์„ ํ•ด๋„, ๋‚˜๋จธ์ง€์—์„œ ๋” ์ƒ์„ธํ•œ ์กฐ์ •์ด ์–ด๋ ค์› ๋‹ค.

๊ทธ๋กœ ์ธํ•ด ๋””์ž์ธ ๊ด€๋ จ props๊ฐ€ ์—ฌ๊ธฐ์ €๊ธฐ ์„ž์ด๋ฉด์„œ ์˜คํžˆ๋ ค ๋ถˆํŽธํ•ด์ง„ ๊ฒƒ์ด์—ˆ๋‹ค.

 

๊ทธ๋ž˜์„œ ์ด๋ถ€๋ถ„์€ ํŒ€ ๊ทœ์น™์œผ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๊ฑธ๋กœ ํ–ˆ๋‹ค.

1. ๋””์ž์ธ ๊ด€๋ จ์ด ์•„๋‹Œ props

2. ๋””์ž์ธ ๊ด€๋ จ props

3. ์ฝœ๋ฐฑ props

4. ๊ฐ ํŒŒํŠธ๋Š” vscode ์ž์ฒด sort ์‚ฌ์šฉ (์ด ๋ถ€๋ถ„์€ ๊ตณ์ด ์—†์–ด๋„ ๋  ๊ฒƒ ๊ฐ™์•„์„œ ์–˜๊ธฐ๋ฅผ ๋” ํ•ด๋ด์•ผ ํ•œ๋‹ค.) 

  // 1. non-styling
  className?: string;
  icon?: IconMapTypes;
  iconPosition?: 'left' | 'right';
  label?: string;
  // 2. styling
  variant?: 'primary' | 'outline' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  disabled?: boolean;	// ์• ๋งคํ•˜์ง€๋งŒ ์ผ๋‹จ ๋‹น์žฅ์€ ์Šคํƒ€์ผ๋ง์—์„œ๋งŒ ์“ฐ์—ฌ์„œ ์—ฌ๊ธฐ ์žˆ๋‹ค.
  // 3. callback
  onClick?: React.MouseEventHandler<HTMLButtonElement>;

 

 

 

4. ๋””์ž์ธ์— ๋”ฐ๋ฅธ ์ขŒ์šฐ padding ์ •๋ ฌ ๋ฌธ์ œ

๋””์ž์ธ์ด ์กฐ๊ธˆ์”ฉ ๋‚˜์™€์„œ ํ”ผ๊ทธ๋งˆ๋ฅผ ํ‹ˆํ‹ˆ์ด ํ™•์ธํ•ด๋ณด๋Š”๋ฐ, ์•„์ด์ฝ˜๊ณผ ํ…์ŠคํŠธ ์ชฝ ํŒจ๋”ฉ๊ฐ’์ด ๋‹ค๋ฅธ ๊ฑธ ํ™•์ธํ•˜๊ณ  ๋””์ž์ด๋„ˆ์—๊ฒŒ ํ™•์ธํ–ˆ๋”๋‹ˆ ์˜๋„๋œ ๊ฑฐ๋ผ๊ณ  ํ–ˆ๋‹ค.

์ž˜ ์•ˆ๋ณด์ด์ง€๋งŒ ์•„์ด์ฝ˜์ชฝ์ด 4px ๋” ์ ์€ ํŒจ๋”ฉ ๊ฐ’์„ ๊ฐ€์ง„๋‹ค.

 

์•„์ง ๋””์ž์ธ ์‹œ์•ˆ์—์„œ๋Š” ์ขŒ์ธก ์•„์ด์ฝ˜๋งŒ ๋“ค์–ด๊ฐ€ ์žˆ๊ณ , ์šฐ์ธก ์•„์ด์ฝ˜์ด ์•ˆ๋‚˜์˜ฌ ์ˆ˜๋„ ์žˆ์ง€๋งŒ ๊ณ ๋ คํ•ด์„œ ๋งŒ๋“ค์–ด๋ณด๊ธฐ๋กœ ํ–ˆ๋‹ค.

tv ํ…Œ์ด๋ธ”์—์„œ tailwind๋กœ ๋ฐ˜์‘ํ˜•๊นŒ์ง€ ๊ด€๋ฆฌํ•˜๊ธฐ์—๋Š” ์—ญํ• ์ด ๋ถ„์‚ฐ๋˜๋Š” ๋А๋‚Œ์ด์–ด์„œ ๋ฐ”๋””์•ˆ์—์„œ ๋™์  ํด๋ž˜์Šค ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค.

// styles ๋‚ด๋ถ€์—์„œ๋Š” ๊ธฐ๋ณธ ํŒจ๋”ฉ๋งŒ ์ฒ˜๋ฆฌ
size: {
  sm: 'h-8 px-4 py-1.5 text-sm',
  md: 'h-10 px-5 py-2 text-base',
  lg: 'h-12 px-6 py-3 text-lg',
},

// ํ•จ์ˆ˜ ๋ฐ”๋”” ๋‚ด์—์„œ ๋ถ„๊ธฐ๋กœ ์ฒ˜๋ฆฌ : icon์ด ์žˆ๋Š” ๊ฒฝ์šฐ, icon ์œ„์น˜์— ๋”ฐ๋ผ ํŒจ๋”ฉ์„ ์ถ”๊ฐ€๋กœ ์คŒ
const buttonLabel = (
  <span
    className={clsx({
      'pr-1': icon && iconPosition === 'left',
      'pl-1': icon && iconPosition === 'right',
    })}
  >
    {label}
  </span>
);

 

 

5. ํ•จ์ˆ˜ ํ‘œํ˜„์‹ vs ํ•จ์ˆ˜ ์„ ์–ธ์‹

const Button = ...

export default Button;

์ผ๊ด€๋œ ์ฝ”๋“œ ์Šคํƒ€์ผ์„ ์œ„ํ•ด ๊ธฐ์กด ํ•จ์ˆ˜ ํ‘œํ˜„์‹์œผ๋กœ ์ž‘์„ฑํ•˜๋˜ ํ˜•ํƒœ๋ฅผ ํ•จ์ˆ˜ ์„ ์–ธ์‹์œผ๋กœ ๋ฐ”๊ฟจ๋‹ค.

 

 

์ „์ฒด ์ฝ”๋“œ

'use client';

import { clsx } from 'clsx';
import React from 'react';
import { tv } from 'tailwind-variants';

import SVGIcon from '../Icons/SVGIcon';
import { IconMapTypes, IconSizeTypes } from '../Icons/icons';

const styles = tv({
  base: 'inline-flex items-center justify-center rounded font-medium whitespace-nowrap transition-all duration-150 outline-none',
  variants: {
    variant: {
      primary: 'cursor-pointer bg-blue-500 text-white hover:bg-blue-600 active:bg-blue-700',
      outline:
        'cursor-pointer border border-gray-300 text-gray-700 hover:bg-gray-50 active:bg-gray-100',
      ghost: 'cursor-pointer bg-transparent text-gray-700 hover:bg-gray-100 active:bg-gray-200',
    },
    size: {
      sm: 'h-8 px-4 py-1.5 text-sm',
      md: 'h-10 px-5 py-2 text-base',
      lg: 'h-12 px-6 py-3 text-lg',
    },
    disabled: {
      true: 'pointer-events-none cursor-not-allowed bg-gray-400 opacity-50',
      false: '',
    },
  },
});

export interface ButtonProps
  extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'disabled' | 'onClick' | 'children'> {
  className?: string;
  icon?: IconMapTypes;
  iconPosition?: 'left' | 'right';
  label?: string;
  variant?: 'primary' | 'outline' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  disabled?: boolean;
  onClick?: React.MouseEventHandler<HTMLButtonElement>;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(
  {
    className,
    icon,
    iconPosition = 'left',
    label,
    type = 'button',
    variant = 'primary',
    size = 'md',
    disabled = false,
    onClick,
    ...rest
  },
  ref
) {
  // STYLES
  const classes = styles({
    variant,
    size,
    disabled,
  });
  const iconSize: IconSizeTypes = size;

  // RENDERING
  const buttonIcon = icon ? (
    <span
      className={clsx('flex-shrink-0', {
        'mr-2': iconPosition === 'left',
        'ml-2': iconPosition === 'right',
      })}
    >
      <SVGIcon icon={icon} size={iconSize} />
    </span>
  ) : null;
  const buttonLabel = (
    <span
      className={clsx({
        'pr-1': icon && iconPosition === 'left',
        'pl-1': icon && iconPosition === 'right',
      })}
    >
      {label}
    </span>
  );

  let content: React.ReactNode[] = [];

  if (buttonIcon) {
    content = iconPosition === 'left' ? [buttonIcon, buttonLabel] : [buttonLabel, buttonIcon];
  } else {
    content = [buttonLabel];
  }

  return (
    <button
      ref={ref}
      className={clsx(classes, className)}
      disabled={disabled}
      type={type}
      aria-disabled={disabled}
      onClick={onClick}
      {...rest}
    >
      {content}
    </button>
  );
});

export default Button;

 

 


3. Trouble Shooting

1. SVGIcon

ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…์ด๋ผ๊ณ ๊นŒ์ง€ ํ•˜๊ธฐ์—๋Š” ์‚ด์ง ๋ฏผ๋งํ•˜์ง€๋งŒ..
์ด์ „ ์ฝ”๋“œ์—๋Š” ์—†๋˜ SVGIcon์„ ์ถ”๊ฐ€ํ–ˆ๋‹ค. 

 

์ฒ˜์Œ์—๋Š” ๋‹จ์ˆœํ•˜๊ฒŒ icon props์˜ type์„ React.ReactElement<IconMap>์œผ๋กœ ์ž‘์„ฑํ–ˆ๋Š”๋ฐ,

์Šคํ† ๋ฆฌ๋ถ์—์„œ ์‚ฌ์šฉํ•ด๋ณด๋‹ˆ๊นŒ ์Šคํ† ๋ฆฌ๋ถ์—์„œ SVGIcon์„ importํ•˜๊ณ , ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ์— <SVGIcon icon={'IC_..'}/> ํ•˜๋ฉด์„œ

์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ๋ณต์žกํ•˜๊ฒŒ ๋А๊ปด์กŒ๋‹ค.

๋˜ํ•œ icon์˜ ์‚ฌ์ด์ฆˆ๋Š” ๋ฒ„ํŠผ์˜ ์‚ฌ์ด์ฆˆ์™€ ๋™์ผํ•ด์•ผํ•˜๋Š”๋ฐ ์ด๋ ‡๊ฒŒ ์‚ฌ์šฉํ•˜๋ฉด  ๋ถ„๋ฆฌ๋˜๋Š” ๊ฒƒ๋„ ๊ฑธ๋ ธ๋‹ค.

 

์–ด์ฐจํ”ผ SVGIcon๋งŒ ์‚ฌ์šฉํ•  ๊ฑฐ๋ผ๋ฉด ์ฐจ๋ผ๋ฆฌ Button ์ปดํฌ๋„ŒํŠธ์—์„œ SVGIcon์„ importํ•˜๊ณ  ์ง์ ‘ ์‚ฌ์šฉํ•˜๋„๋ก ํ•˜๊ณ ,

icon props๋กœ๋Š” ํ•ด๋‹นํ•˜๋Š” icon์˜ type๋งŒ ๋„˜๊ธฐ๋Š” ๊ฒŒ ์ข‹์ง€ ์•Š์„๊นŒํ•˜๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค.

 

Button ์ปดํฌ๋„ŒํŠธ์—์„œ SVGIcon, IconMapTypes, IconSizeTypes๋ฅผ ํ˜ธ์ถœํ–ˆ๋‹ค.

import SVGIcon from '../Icons/SVGIcon';
import { IconMapTypes, IconSizeTypes } from '../Icons/icons';

 

icon์˜ props๋ฅผ IconMapTypes๋กœ ์ •์˜ํ•˜๋ฉด, ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ icon์œผ๋กœ ๋„˜๊ธธ ์ˆ˜ ์žˆ๋Š” ๊ฐ’๋“ค์„ ์‰ฝ๊ฒŒ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

icon?: IconMapTypes;

 

Button ํ•จ์ˆ˜ ๋ฐ”๋”” ๋‚ด์—์„œ iconSize๋ฅผ ์ •์˜ํ–ˆ๋‹ค.

Button์˜ props๋กœ ๋ฐ›์€ size๋ฅผ iconSize์— ๊ทธ๋Œ€๋กœ ์ ์šฉํ•œ๋‹ค.

  const iconSize: IconSizeTypes = size;

 

Button ๋‚ด๋ถ€์—์„œ SVGIcon์„ ์ด๋ ‡๊ฒŒ ์‚ฌ์šฉํ•˜๋ฉด ์™ธ๋ถ€์—์„œ๋Š” icon์„ ๋„ฃ๊ณ  ์‹ถ์„ ๋•Œ ๋‹จ์ˆœ string ํ˜•์˜ ์•„์ด์ฝ˜ ํƒ€์ž…๊ฐ’๋งŒ ์ „๋‹ฌํ•˜๋ฉด ๋œ๋‹ค.

<SVGIcon icon={icon} size={iconSize} />

 

 

2. tailwind-variant, tailwind-merge

์ฒ˜์Œ prettier-plugin-tailwindcss๋ฅผ ์ ์šฉํ–ˆ๋Š”๋ฐ tailwind ์ •๋ ฌ์ด ์•ˆ๋˜๋Š” ๊ฑฐ๋‹ค.

๋˜ ๋ญ”๊ฐ€ ์„ธํŒ…์ด ๋œ ๋๋‚˜ ๊ฝค ํ•œ์ฐธ ํ—ค๋ฉจ๋Š”๋ฐ ์•Œ๊ณ ๋ณด๋‹ˆ ์ด ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ๋ณ€์ˆ˜์— ํ• ๋‹นํ•œ tailwind๋Š” ์ •๋ ฌ์„ ๋ชปํ•ด์ค€๋‹ค๋Š” ๊ฒƒ์ด์—ˆ๋‹ค.

 

clsx๋กœ ๊ฐ์‹ธ๊ฑฐ๋‚˜ tv๋ฅผ ์„ค์น˜ํ•ด์„œ ๊ทธ๊ฑธ๋กœ ๊ฐ์‹ธ์•ผ ํ•œ๋‹ค๋Š”๋ฐ ๊ทธ๋Ÿฌ๋ฉด ๋„ˆ๋ฌด ์ฝ”๋“œ๊ฐ€ ์ง€์ €๋ถ„ํ•ด์งˆ ๊ฒƒ ๊ฐ™์•„์„œ ์ฐจ๋ผ๋ฆฌ

tv๋ฅผ ์„ค์น˜ํ•˜๋˜, ์Šคํƒ€์ผ ๊ด€๋ จ ์ฝ”๋“œ๋ฅผ ์•„์˜ˆ ๋”ฐ๋กœ ๋นผ๊ธฐ๋กœ ํ–ˆ๋‹ค.

 

 

๊ทธ๋ ‡๊ฒŒ ์ •๋ ฌ์„ ๋ฌด์‚ฌํžˆ ํ•˜๊ณ  ๋‚˜๋‹ˆ๊นŒ ์ด์–ด์„œ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ..

Error message์— ๋ณด๋ฉด tailwind-merge๋ฅผ resolve ํ•  ์ˆ˜ ์—†๋‹ค๊ณ  ๋‚˜์˜จ๋‹ค.

 

tailwind-variants์—์„œ ์›๋ž˜ tailwind-merge๋ฅผ ํฌํ•จํ•ด์„œ ์ง€์›ํ–ˆ๋Š”๋ฐ, ์—…๋ฐ์ดํŠธ ํ•˜๋ฉด์„œ ํฌํ•จ์‹œํ‚ค์ง€ ์•Š๊ฒŒ ๋˜์—ˆ๋‹ค๋Š” ๊ฑฐ๋‹ค.

์›๋ž˜๋Š” ๋ฌด์กฐ๊ฑด tailwind-merge๋ฅผ ๋Œ๋ ธ๋Š”๋ฐ, ์—…๋ฐ์ดํŠธ ์ดํ›„๋Š” ์„ ํƒ์ด ๊ฐ€๋Šฅํ•ด์กŒ๋‹ค๋Š” ๋ง.

 

ํ•˜์ง€๋งŒ ์Šคํ† ๋ฆฌ๋ถ์€  ๋ฒˆ๋“ค๋ง ์‹œ์ ์—์„œ ๋ชจ๋“  import๋ฅผ ์ •์ ์œผ๋กœ ๋ถ„์„ํ•˜์—ฌ, tailwind-variants ๋‚ด๋ถ€์—์„œ tailwind-merge๋ฅผ ์„ ํƒ์ ์œผ๋กœ import ํ•˜๋”๋ผ๋„ ์ด ๋ชจ๋“ˆ์ด ํ•„์ˆ˜๋ผ๊ณ  ๊ฐ€์ •ํ•˜๊ณ  ์‹œ๋„ํ•œ๋‹ค๋Š” ๊ฑฐ๋‹ค.

 

๊ฒฐ๋ก ์€ tailwind-merge๋„ ์„ค์น˜ํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค๋Š” ๋ง์ด๋‹ค.


4. ๋งˆ๋ฌด๋ฆฌ

์‚ฌ์‹ค ์ด๋ ‡๊ฒŒ ๊ผผ๊ผผํ•˜๊ฒŒ ์ฐพ์•„๊ฐ€๋ฉด์„œ ๋งŒ๋“ค์–ด๋ดค์ง€๋งŒ, ์•„์ง๋„ ๊ฑธ๋ฆฌ๋Š” ๋ถ€๋ถ„์€ ์—ฌ์ „ํžˆ ๋งŽ๋‹ค.

  • children์„ ๋ฐ›์ง€ ์•Š์•„๋„ ๊ดœ์ฐฎ์€๊ฐ€
  • icon์— ๋Œ€ํ•œ ๋กœ์ง์„ ์ €๋ ‡๊ฒŒ if ๋ถ„๊ธฐ๋ฌธ์œผ๋กœ ์‚ด์ง ๋ณต์žกํ•˜๊ฒŒ ๋‚จ๊ฒจ๋„ ๊ดœ์ฐฎ์€๊ฐ€
  • ์Šคํƒ€์ผ๋ง์„ ์ •์˜ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์ €๊ฒŒ ์ตœ์„ ์ผ ๊นŒ
  • ์ถ”ํ›„์— Icon ์ž๋ฆฌ์— SVG ์•„์ด์ฝ˜์ด ์•„๋‹Œ ๋‹ค๋ฅธ ํ˜•ํƒœ๊ฐ€ ๋“ค์–ด๊ฐ€๋„๋ก ๋ณ€๊ฒฝ๋˜๋Š” ๊ฒฝ์šฐ๋„ ์ƒ๊ธธ๊นŒ
  • ...

ํ•˜์ง€๋งŒ ์ผ๋‹จ ํ˜„์žฌ๋กœ์จ๋Š” ์ด๋Œ€๋กœ ๋งˆ๋ฌด๋ฆฌ ์ง“๊ธฐ๋กœ ํ–ˆ๋‹ค.

์ ์–ด๋„ ์ง€๊ธˆ๊นŒ์ง€ ๋‚˜์˜จ ๊ธฐํš/๋””์ž์ธ ๋‚ด์—์„œ๋Š” ์ถฉ๋ถ„ํžˆ ๊ตฌ์กฐ๋ฅผ ์žก์€ ๊ฒƒ ๊ฐ™๊ณ , ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ๋“ค๋„ ์„œ๋‘˜๋Ÿฌ์„œ ์ง„ํ–‰ํ•ด์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

๋‹ค๋ฅธ ํ”„๋ก ํŠธ ์นœ๊ตฌ๊ฐ€ ๋‹น์‹œ ์ผ์ด ์žˆ์–ด์„œ ๊ธฐ๋ณธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋Œ€๋ถ€๋ถ„ ๋‚ด๊ฐ€ ๋งŒ๋“ค์–ด์„œ ๋‚ด๊ฐ€ ๋งˆ๋ฌด๋ฆฌ๋ฅผ ์ง€์–ด์•ผ ํ•œ๋‹ค..

 

Button ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ฐธ๊ณ ํ•˜์—ฌ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ๋“ค๋„ ๋” ํƒ„ํƒ„ํ•˜๊ฒŒ ์ˆ˜์ •ํ•  ์ƒ๊ฐ์ด๋‹ค.

 

 

 

 

 

 

์ฐธ๊ณ 

๋”๋ณด๊ธฐ

https://developer.mozilla.org/en-US/docs/Web/API/HTMLButtonElement

 

HTMLButtonElement - Web APIs | MDN

Inherits properties from its parent, HTMLElement. HTMLButtonElement.command A string value indicating the action to be performed on an element being controlled by this button. HTMLButtonElement.commandForElement A reference to an existing Element that the

developer.mozilla.org

https://velog.io/@dongkyun/TS-HTMLElement%EC%9D%98-type-%EC%83%81%EC%86%8D%EB%B0%9B%EA%B8%B0

 

[TS] HTMLElement์˜ type ์ƒ์†๋ฐ›๊ธฐ

๋‹ค์Œ ์ฝ”๋“œ๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. props๋กœ ๋„˜์–ด์˜ค๋Š” children๊ณผ rest์— ๋Œ€ํ•œ ํƒ€์ž… ์ •์˜๊ฐ€ ์ด๋ฃจ์–ด์ง€์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.์œ„์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๊ฐ€์žฅ ๊ฐ„ํŽธํ•œ ํ•ด๊ฒฐ๋ฐฉ๋ฒ•์ด๋‹ค. onClick ํ•จ์ˆ˜๋Š” HTMLButto

velog.io

https://github.com/tailwindlabs/prettier-plugin-tailwindcss

 

GitHub - tailwindlabs/prettier-plugin-tailwindcss: A Prettier plugin for Tailwind CSS that automatically sorts classes based on

A Prettier plugin for Tailwind CSS that automatically sorts classes based on our recommended class order. - tailwindlabs/prettier-plugin-tailwindcss

github.com

https://www.tailwind-variants.org/docs/introduction#automatic-conflict-resolution

 

Introduction – Tailwind Variants

 

www.tailwind-variants.org

https://velog.io/@clydehan/JavaScript-%ED%95%A8%EC%88%98-%EC%84%A0%EC%96%B8%EC%8B%9D-vs-%ED%95%A8%EC%88%98-%ED%91%9C%ED%98%84%EC%8B%9D-%EC%B0%A8%EC%9D%B4%EC%A0%90%EA%B3%BC-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-%EC%A0%84%EB%9E%B5

 

JavaScript ํ•จ์ˆ˜ ์„ ์–ธ์‹ vs ํ•จ์ˆ˜ ํ‘œํ˜„์‹: ์ฐจ์ด์ ๊ณผ ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ „๋žต

JavaScript ํ•จ์ˆ˜ ์„ ์–ธ์‹๊ณผ ํ•จ์ˆ˜ ํ‘œํ˜„์‹์˜ ์ฐจ์ด: ํ˜ธ์ด์ŠคํŒ…, ์„ฑ๋Šฅ, ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ ์ธก๋ฉด ๋น„๊ต

velog.io

 

๋ฐ˜์‘ํ˜•