React TypeScript 컴포넌트
재사용 가능한 React 함수형 컴포넌트 템플릿. Props 타입, 상태 관리, 이벤트 핸들러 패턴 포함.
import React, { useState, useCallback } from 'react';
type Variant = 'primary' | 'secondary' | 'danger' | 'ghost';
interface ButtonProps {
label: string;
variant?: Variant;
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
loading?: boolean;
fullWidth?: boolean;
onClick?: () => void | Promise<void>;
}
const variantClasses: Record<Variant, string> = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 border-blue-600',
secondary: 'bg-gray-100 text-gray-800 hover:bg-gray-200 border-gray-300',
danger: 'bg-red-600 text-white hover:bg-red-700 border-red-600',
ghost: 'bg-transparent text-gray-700 hover:bg-gray-100 border-transparent',
};
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
const Spinner: React.FC = () => (
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z" />
</svg>
);
const Button: React.FC<ButtonProps> = ({
label,
variant = 'primary',
size = 'md',
disabled = false,
loading = false,
fullWidth = false,
onClick,
}) => {
const [isPressed, setIsPressed] = useState(false);
const isDisabled = disabled || loading;
const handleClick = useCallback(async () => {
if (isDisabled) return;
setIsPressed(true);
try {
await onClick?.();
} finally {
setTimeout(() => setIsPressed(false), 100);
}
}, [isDisabled, onClick]);
return (
<button
type="button"
onClick={handleClick}
disabled={isDisabled}
aria-busy={loading}
aria-disabled={isDisabled}
className={[
'inline-flex items-center justify-center gap-2 rounded-md border font-medium',
'transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500',
variantClasses[variant],
sizeClasses[size],
fullWidth ? 'w-full' : '',
isPressed ? 'scale-95' : '',
isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
].join(' ')}
>
{loading && <Spinner />}
{label}
</button>
);
};
export default Button;