Controls 애드온은 Storybook UI에서 컴포넌트의 args(props)를 동적으로 조작할 수 있게 해주는 강력한 도구입니다.
브라우저에서 props 변경
TypeScript 기반
10+ 컨트롤 타입
import type { Meta, StoryObj } from '@storybook/react';
import { Input } from './Input';
const meta: Meta<typeof Input> = {
title: 'Components/Input',
component: Input,
argTypes: {
// 텍스트 입력
label: {
control: 'text',
description: '입력 필드 레이블',
table: {
type: { summary: 'string' },
defaultValue: { summary: '' },
},
},
// Select 드롭다운
variant: {
control: 'select',
options: ['outlined', 'filled', 'standard'],
description: '입력 필드 스타일',
},
// Radio 버튼
size: {
control: 'radio',
options: ['small', 'medium', 'large'],
description: '입력 필드 크기',
},
// 범위 슬라이더
maxLength: {
control: { type: 'range', min: 10, max: 200, step: 10 },
description: '최대 문자 길이',
},
// 색상 선택
borderColor: {
control: 'color',
description: '테두리 색상',
},
// 불린 체크박스
required: {
control: 'boolean',
description: '필수 입력 여부',
},
// 날짜 선택
defaultDate: {
control: 'date',
description: '기본 날짜',
},
// 배열 입력
tags: {
control: 'object',
description: '태그 배열',
},
// 함수 액션
onChange: {
action: 'changed',
description: '값 변경 이벤트',
},
// 컨트롤 비활성화
internalState: {
control: false,
description: '내부 상태 (편집 불가)',
},
},
};
export default meta;
type Story = StoryObj<typeof Input>;text텍스트 입력boolean체크박스number숫자 입력range슬라이더color색상 선택기date날짜 선택기select드롭다운radio라디오 버튼check체크박스 그룹objectJSON 편집기// 조건부 컨트롤
argTypes: {
theme: {
control: 'select',
options: ['light', 'dark'],
if: { arg: 'advanced', truthy: true },
},
// 카테고리 그룹화
backgroundColor: {
control: 'color',
table: { category: 'Style' },
},
borderRadius: {
control: 'number',
table: { category: 'Style' },
},
// 정렬 순서
title: {
control: 'text',
table: {
category: 'Content',
subcategory: 'Text',
},
},
// Mapping (enum 변환)
status: {
control: 'select',
options: ['Success', 'Error', 'Warning'],
mapping: {
Success: 'success',
Error: 'error',
Warning: 'warning',
},
},
}// Card.tsx
export interface CardProps {
title: string;
description: string;
imageUrl?: string;
variant: 'elevated' | 'outlined' | 'filled';
padding: 'none' | 'small' | 'medium' | 'large';
cornerRadius: number;
clickable: boolean;
badge?: string;
onCardClick?: () => void;
}
export const Card: React.FC<CardProps> = ({
title,
description,
imageUrl,
variant = 'elevated',
padding = 'medium',
cornerRadius = 8,
clickable = false,
badge,
onCardClick,
}) => {
const variantStyles = {
elevated: 'shadow-lg bg-white',
outlined: 'border-2 border-gray-300 bg-white',
filled: 'bg-gray-100',
};
const paddingStyles = {
none: 'p-0',
small: 'p-2',
medium: 'p-4',
large: 'p-6',
};
return (
<div
className={`${variantStyles[variant]} ${paddingStyles[padding]}
${clickable ? 'cursor-pointer hover:scale-105' : ''}
transition-transform`}
style={{ borderRadius: `${cornerRadius}px` }}
onClick={clickable ? onCardClick : undefined}
>
{badge && (
<span className="inline-block bg-blue-600 text-white text-xs px-2 py-1 rounded mb-2">
{badge}
</span>
)}
{imageUrl && (
<img src={imageUrl} alt={title} className="w-full h-48 object-cover mb-4" />
)}
<h3 className="text-xl font-bold mb-2">{title}</h3>
<p className="text-gray-600">{description}</p>
</div>
);
};// Card.stories.tsx
const meta: Meta<typeof Card> = {
title: 'Components/Card',
component: Card,
parameters: {
layout: 'centered',
},
argTypes: {
// 기본 콘텐츠
title: {
control: 'text',
description: '카드 제목',
table: { category: 'Content' },
},
description: {
control: 'text',
description: '카드 설명',
table: { category: 'Content' },
},
imageUrl: {
control: 'text',
description: '이미지 URL',
table: { category: 'Content' },
},
badge: {
control: 'text',
description: '뱃지 텍스트',
table: { category: 'Content' },
},
// 스타일링
variant: {
control: 'select',
options: ['elevated', 'outlined', 'filled'],
description: '카드 스타일 변형',
table: { category: 'Style' },
},
padding: {
control: 'radio',
options: ['none', 'small', 'medium', 'large'],
description: '내부 여백',
table: { category: 'Style' },
},
cornerRadius: {
control: { type: 'range', min: 0, max: 32, step: 4 },
description: '모서리 둥글기',
table: { category: 'Style' },
},
// 인터랙션
clickable: {
control: 'boolean',
description: '클릭 가능 여부',
table: { category: 'Interaction' },
},
onCardClick: {
action: 'card clicked',
description: '카드 클릭 핸들러',
table: { category: 'Interaction' },
},
},
};
export default meta;
type Story = StoryObj<typeof Card>;
// 기본 Story
export const Default: Story = {
args: {
title: 'Beautiful Card',
description: 'This is a customizable card component with various options.',
variant: 'elevated',
padding: 'medium',
cornerRadius: 8,
clickable: false,
},
};
// 이미지 포함
export const WithImage: Story = {
args: {
...Default.args,
imageUrl: 'https://picsum.photos/400/300',
badge: 'New',
},
};
// 인터랙티브
export const Interactive: Story = {
args: {
...Default.args,
clickable: true,
variant: 'outlined',
},
};
// Playground (모든 컨트롤 활성화)
export const Playground: Story = {
args: {
title: 'Playground Card',
description: 'Try changing the controls!',
imageUrl: 'https://picsum.photos/400/300',
variant: 'elevated',
padding: 'medium',
cornerRadius: 16,
clickable: true,
badge: 'Featured',
},
};control: false내부 상태나 자동 계산 값은 컨트롤 비활성화
table: { disable: false }문서화만 하고 컨트롤 제외