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 ์ ๋ ฌ ๋ฌธ์
๋์์ธ์ด ์กฐ๊ธ์ฉ ๋์์ ํผ๊ทธ๋ง๋ฅผ ํํ์ด ํ์ธํด๋ณด๋๋ฐ, ์์ด์ฝ๊ณผ ํ ์คํธ ์ชฝ ํจ๋ฉ๊ฐ์ด ๋ค๋ฅธ ๊ฑธ ํ์ธํ๊ณ ๋์์ด๋์๊ฒ ํ์ธํ๋๋ ์๋๋ ๊ฑฐ๋ผ๊ณ ํ๋ค.

์์ง ๋์์ธ ์์์์๋ ์ข์ธก ์์ด์ฝ๋ง ๋ค์ด๊ฐ ์๊ณ , ์ฐ์ธก ์์ด์ฝ์ด ์๋์ฌ ์๋ ์์ง๋ง ๊ณ ๋ คํด์ ๋ง๋ค์ด๋ณด๊ธฐ๋ก ํ๋ค.
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
JavaScript ํจ์ ์ ์ธ์ vs ํจ์ ํํ์: ์ฐจ์ด์ ๊ณผ ์ฑ๋ฅ ์ต์ ํ ์ ๋ต
JavaScript ํจ์ ์ ์ธ์๊ณผ ํจ์ ํํ์์ ์ฐจ์ด: ํธ์ด์คํ , ์ฑ๋ฅ, ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ ์ธก๋ฉด ๋น๊ต
velog.io
GitHub