Tailwind CSS Academy · 강의

컴포넌트 라이브러리 구축

토큰을 사용하여 재사용 가능한 컴포넌트 10개를 만들고, CVA로 일관된 변형 API와 접근성 높은 마크업 패턴을 적용합니다.

레슨 3/413개 단계

컴포넌트 라이브러리 구축은(는) CoddyKit의 무료 Tailwind CSS Academy 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Tailwind CSS Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Tailwind CSS Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

Component Library Construction Goals

Building the component library means translating the token and config layer into a set of usable, documented UI primitives. Each component should use the semantic tokens defined in the previous step, expose a typed variant API, meet WCAG AA accessibility standards, and have at least one Storybook story. This lesson walks through building ten representative components using these principles.

Button Component With CVA

The Button is typically the first component in any library. Use class-variance-authority (CVA) to define typed variant and size props. CVA maps prop combinations to class strings, producing clean and predictable class output. The component accepts variant (primary, secondary, ghost, danger) and size (sm, md, lg) props, with full TypeScript inference.

import { cva, type VariantProps } from 'class-variance-authority';

const buttonVariants = cva(
  // Base classes shared by all variants
  'inline-flex items-center justify-center rounded-button font-semibold transition focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
  {
    variants: {
      variant: {
        primary:   'bg-primary text-white hover:bg-primary-hover focus:ring-primary',
        secondary: 'bg-surface-elevated text-text-primary border border-border hover:bg-gray-100',
        ghost:     'text-primary hover:bg-primary-light',
        danger:    'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
      },
      size: {
        sm: 'px-3 py-1.5 text-sm',
        md: 'px-4 py-2 text-sm',
        lg: 'px-6 py-3 text-base',
      },
    },
    defaultVariants: { variant: 'primary', size: 'md' },
  }
);

type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
  VariantProps<typeof buttonVariants>;

export function Button({ variant, size, className, ...props }: ButtonProps) {
  return <button className={buttonVariants({ variant, size, className })} {...props} />;
}

Badge Component

The Badge is a small inline label used for status indicators and category tags. It uses CVA with color variants mapped to semantic meaning: default (gray), success (green), warning (yellow), danger (red), info (blue). The base style creates the pill shape common to all variants.

import { cva, type VariantProps } from 'class-variance-authority';

const badgeVariants = cva(
  'inline-flex items-center rounded-badge px-2.5 py-0.5 text-xs font-semibold',
  {
    variants: {
      variant: {
        default: 'bg-gray-100 text-gray-800',
        success: 'bg-green-100 text-green-800',
        warning: 'bg-yellow-100 text-yellow-800',
        danger:  'bg-red-100 text-red-800',
        info:    'bg-blue-100 text-blue-800',
      },
    },
    defaultVariants: { variant: 'default' },
  }
);

type BadgeProps = React.HTMLAttributes<HTMLSpanElement> &
  VariantProps<typeof badgeVariants>;

export function Badge({ variant, className, ...props }: BadgeProps) {
  return <span className={badgeVariants({ variant, className })} {...props} />;
}

Input Component

The Input component wraps a native input with consistent styling and forwarded refs for form library compatibility. Include a label and optional error message as part of the component's API. The error state changes the border and ring color from gray to red, providing immediate visual feedback without requiring extra CSS.

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

interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label?: string;
  error?: string;
}

export const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ label, error, className, id, ...props }, ref) => (
    <div className='flex flex-col gap-1'>
      {label && (
        <label htmlFor={id} className='text-sm font-medium text-text-primary'>
          {label}
        </label>
      )}
      <input
        ref={ref}
        id={id}
        aria-invalid={Boolean(error)}
        aria-describedby={error ? `${id}-error` : undefined}
        className={clsx(
          'w-full rounded-input border px-3 py-2 text-sm text-text-primary transition',
          'focus:outline-none focus:ring-2 focus:ring-offset-0',
          error
            ? 'border-red-500 focus:ring-red-500'
            : 'border-border focus:border-primary focus:ring-primary',
          className
        )}
        {...props}
      />
      {error && (
        <p id={`${id}-error`} role='alert' className='text-xs text-red-600'>
          {error}
        </p>
      )}
    </div>
  )
);

Card Component

The Card is a container component with optional CardHeader, CardBody, and CardFooter sub-components. Use the compound component pattern — export related pieces from the same file and name them with dot notation or individual exports. The Card uses the shadow-card and bg-surface semantic tokens defined in the config.

// Card.tsx
export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={clsx('rounded-card bg-surface shadow-card overflow-hidden', className)}
      {...props}
    />
  );
}

export function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={clsx('border-b border-border px-6 py-4', className)}
      {...props}
    />
  );
}

export function CardBody({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return <div className={clsx('px-6 py-4', className)} {...props} />;
}

export function CardFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={clsx('border-t border-border bg-surface-elevated px-6 py-3', className)}
      {...props}
    />
  );
}

Avatar Component

The Avatar displays a user photo or falls back to initials in a colored circle. Use a size variant for small (8), medium (10), and large (14) sizes. When no image is provided, display initials extracted from the user's name in a deterministic background color keyed to the name's first character. Add a status dot variant for online/offline/busy indicators.

const avatarSize = cva('rounded-full overflow-hidden flex-shrink-0', {
  variants: {
    size: {
      sm: 'h-8 w-8 text-xs',
      md: 'h-10 w-10 text-sm',
      lg: 'h-14 w-14 text-base',
    },
  },
  defaultVariants: { size: 'md' },
});

export function Avatar({ src, name, size }: AvatarProps) {
  const initials = name?.split(' ').map(n => n[0]).join('').slice(0, 2).toUpperCase();

  return (
    <div className={avatarSize({ size })}>
      {src ? (
        <img src={src} alt={name} className='h-full w-full object-cover' />
      ) : (
        <div className='flex h-full w-full items-center justify-center
                        bg-primary-light font-semibold text-primary'>
          {initials}
        </div>
      )}
    </div>
  );
}

Spinner Loading Component

A Spinner communicates loading state. Use Tailwind's animate-spin on an SVG with a transparent track and a colored arc. Size variants match those of the Button component so a Spinner placed inside a loading button renders at the correct size. Include role='status' and a visually hidden label for screen readers.

const spinnerSize = cva('animate-spin', {
  variants: {
    size: { sm: 'h-4 w-4', md: 'h-5 w-5', lg: 'h-6 w-6' },
  },
  defaultVariants: { size: 'md' },
});

export function Spinner({ size }: { size?: 'sm' | 'md' | 'lg' }) {
  return (
    <svg
      className={spinnerSize({ size })}
      xmlns='http://www.w3.org/2000/svg'
      fill='none'
      viewBox='0 0 24 24'
      role='status'
      aria-label='Loading'
    >
      <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-8V0C5.373 0 0 5.373 0 12h4z'
      />
    </svg>
  );
}

Tooltip Component

A Tooltip shows supplementary information on hover or focus. The simplest implementation uses CSS-only with group and absolute positioning. The trigger wraps in a group relative container, and the tooltip div uses absolute bottom-full mb-2 hidden group-hover:block to appear above the trigger on hover. Add role='tooltip' and an aria-describedby link for accessibility.

export function Tooltip({ content, children }: TooltipProps) {
  return (
    <div className='group relative inline-block'>
      {children}
      <div
        role='tooltip'
        className='pointer-events-none absolute bottom-full left-1/2 z-10
                   mb-2 -translate-x-1/2 whitespace-nowrap rounded-lg
                   bg-gray-900 px-3 py-1.5 text-xs text-white opacity-0
                   transition-opacity group-hover:opacity-100'
      >
        {content}
        {/* Arrow */}
        <div className='absolute left-1/2 top-full -translate-x-1/2
                        border-4 border-transparent border-t-gray-900' />
      </div>
    </div>
  );
}

Toast Notification Component

Toast notifications appear at the corner of the screen and dismiss automatically. Use fixed bottom-4 right-4 z-50 flex flex-col gap-2 for the container that holds multiple toasts. Each toast has a colored left border for the severity variant, an icon, a message, and a close button. Animate entrance and exit with CSS transitions on opacity and translateY.

const toastVariants = cva(
  'flex items-start gap-3 rounded-card bg-surface shadow-elevated border-l-4 p-4 min-w-[300px]',
  {
    variants: {
      severity: {
        success: 'border-green-500',
        error:   'border-red-500',
        warning: 'border-yellow-500',
        info:    'border-blue-500',
      },
    },
    defaultVariants: { severity: 'info' },
  }
);

export function Toast({ severity, message, onClose }: ToastProps) {
  return (
    <div className={toastVariants({ severity })} role='alert'>
      <div className='flex-1'>
        <p className='text-sm font-medium text-text-primary'>{message}</p>
      </div>
      <button onClick={onClose} className='text-text-secondary hover:text-text-primary'>
        <span className='sr-only'>Dismiss</span>
        &times;
      </button>
    </div>
  );
}

Component Storybook Stories

Each component needs at least one Storybook story per meaningful variant. Stories serve as living documentation and visual regression baselines. Use the CSF3 format (Component Story Format) with named exports for each story. Include a Default, a story per variant, and an AllVariants showcase story that renders all variants side by side for quick comparison.

// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  component: Button,
  title: 'Primitives/Button',
};
export default meta;
type Story = StoryObj<typeof Button>;

export const Default: Story = {
  args: { children: 'Click me', variant: 'primary', size: 'md' },
};

export const Secondary: Story = {
  args: { children: 'Cancel', variant: 'secondary' },
};

export const AllVariants: Story = {
  render: () => (
    <div className='flex flex-wrap gap-4'>
      <Button variant='primary'>Primary</Button>
      <Button variant='secondary'>Secondary</Button>
      <Button variant='ghost'>Ghost</Button>
      <Button variant='danger'>Danger</Button>
    </div>
  ),
};

Component Unit Testing

Test component behavior — not styling. Focus tests on: correct ARIA attributes are rendered, click handlers fire, disabled state prevents interaction, and error messages appear when the error prop is set. Use @testing-library/react with its accessibility-focused queries like getByRole and getByLabelText rather than querying by class names, which couples tests to styling implementation details.

// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';

test('calls onClick when clicked', () => {
  const handleClick = jest.fn();
  render(<Button onClick={handleClick}>Save</Button>);
  fireEvent.click(screen.getByRole('button', { name: 'Save' }));
  expect(handleClick).toHaveBeenCalledTimes(1);
});

test('does not call onClick when disabled', () => {
  const handleClick = jest.fn();
  render(<Button disabled onClick={handleClick}>Save</Button>);
  fireEvent.click(screen.getByRole('button', { name: 'Save' }));
  expect(handleClick).not.toHaveBeenCalled();
});

Quick Check

Test your understanding of Tailwind CSS Mastery concepts from this lesson.

Lesson Recap

In this lesson you learned: building typed components with CVA for Button, Badge, Input, and Card using semantic design tokens, writing accessible components with ARIA attributes, screen reader labels, and keyboard support, and creating Storybook stories and unit tests for each component. Next up is the final lesson: documenting and handing off the design system.

무료로 시작

AI 튜터와 함께 HTML을(를) 배우세요 — 무료

브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.

코스
30
레슨
120

자주 묻는 질문

“컴포넌트 라이브러리 구축” 강의는 무료인가요?

네 — “컴포넌트 라이브러리 구축” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Tailwind CSS Academy 강의 전체를 잠금 해제할 수 있습니다. Tailwind CSS Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“컴포넌트 라이브러리 구축”에서 뭘 배우나요?

토큰을 사용하여 재사용 가능한 컴포넌트 10개를 만들고, CVA로 일관된 변형 API와 접근성 높은 마크업 패턴을 적용합니다. 브라우저에서 직접 실행하는 실습 코드로 Tailwind CSS Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Tailwind CSS Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Tailwind CSS Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.

“컴포넌트 라이브러리 구축” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Tailwind CSS Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Tailwind CSS Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 디자인 시스템 계획하기
  2. 토큰 및 설정 계층 구축
  3. 컴포넌트 라이브러리 구축
  4. 문서화 및 팀 인계
← Tailwind CSS Academy(으)로 돌아가기