본문으로 바로가기
리옵트 핸드북
리옵트 핸드북
AI 시대의 디자인 시스템

기반 설계

토큰 아키텍처컴포넌트 명세접근성 내장

AI 워크플로우

프롬프트 인터페이스DESIGN.md 인터페이스에이전틱 디자인 품질 제어워크플로우 전략AI 기반 DS 진화컨텍스트 주입

사용자 경험

일관성 패턴인터랙션 설계폼 & 데이터 입력

차세대 인터페이스

에이전트 UI 프로토콜생성형 UI공간·멀티모달 인터페이스

실전

실행 플레이북마이그레이션 전략거버넌스 & 협업사례 연구

운영

검증 체크리스트업데이트 로그
핸드북›AI 시대의 디자인 시스템›인터랙션 설계
한국어English

인터랙션 설계

상태 피드백, 마이크로인터랙션, duration·easing·애니메이션 프리셋의 토큰화와 prefers-reduced-motion 지원으로 AI가 일관된 인터랙션을 구현하게 하는 설계.

핵심 요약

  • 버튼·입력의 모든 상태(default·hover·focus·active·disabled·loading·error)를 스타일 토큰으로 정의하고, 상태 전이 다이어그램으로 흐름을 명시합니다.
  • 마이크로인터랙션(button press, hover lift, focus ring, ripple 등)을 scale·duration·easing 값으로 토큰화하고 Tailwind 인터랙션 클래스로 제공합니다.
  • Duration은 instant(50ms)~slowest(500ms) 스케일로, easing은 easeOut·easeIn·easeInOut·spring·bounce 함수로 표준화합니다.
  • fadeIn·slideUp·scaleIn 같은 애니메이션 프리셋을 정의해 CSS keyframe으로 출력합니다.
  • prefers-reduced-motion은 useReducedMotion 훅과 CSS 미디어 쿼리로 받아 접근성을 챙깁니다.

사용자 경험의 품질은 인터랙션의 디테일에서 갈립니다. AI가 일관된 인터랙션을 구현하려면 상태 피드백과 애니메이션을 먼저 토큰으로 정리해 둬야 합니다.

인터랙션 계층

상태 피드백 패턴

상태 전이 다이어그램

상태별 스타일 토큰

// tokens/states.ts

export const interactiveStates = {
  button: {
    default: {
      background: '{colors.primary}',
      foreground: '{colors.primary-foreground}',
      border: 'transparent',
      shadow: 'none',
    },
    hover: {
      background: '{colors.primary-hover}',
      transform: 'translateY(-1px)',
      shadow: '{shadows.sm}',
    },
    focus: {
      outline: '2px solid {colors.ring}',
      outlineOffset: '2px',
    },
    active: {
      background: '{colors.primary-active}',
      transform: 'translateY(0)',
      shadow: 'none',
    },
    disabled: {
      background: '{colors.muted}',
      foreground: '{colors.muted-foreground}',
      cursor: 'not-allowed',
      opacity: 0.5,
    },
    loading: {
      cursor: 'wait',
      pointerEvents: 'none',
    },
  },

  input: {
    default: {
      border: '{colors.border}',
      background: '{colors.background}',
    },
    hover: {
      border: '{colors.border-hover}',
    },
    focus: {
      border: '{colors.ring}',
      ring: '1px solid {colors.ring}',
    },
    error: {
      border: '{colors.destructive}',
      ring: '1px solid {colors.destructive}',
    },
    disabled: {
      background: '{colors.muted}',
      cursor: 'not-allowed',
    },
  },
} as const

피드백 컴포넌트

// components/feedback/loading-button.tsx

interface LoadingButtonProps extends ButtonProps {
  loading?: boolean
  loadingText?: string
}

/**
 * 로딩 상태가 있는 버튼
 * @ai-hint 비동기 액션(폼 제출, API 호출) 시 사용
 */
export function LoadingButton({
  children,
  loading,
  loadingText,
  disabled,
  ...props
}: LoadingButtonProps) {
  return (
    <Button
      disabled={disabled || loading}
      aria-busy={loading}
      {...props}
    >
      {loading && (
        <Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden="true" />
      )}
      {loading ? (loadingText ?? children) : children}
    </Button>
  )
}

마이크로인터랙션

인터랙션 유형

마이크로인터랙션 토큰

// tokens/interactions.ts

export const interactions = {
  // 버튼 누름 효과
  buttonPress: {
    scale: 0.98,
    duration: '100ms',
    easing: 'ease-out',
  },

  // 호버 리프트 효과
  hoverLift: {
    translateY: '-2px',
    shadow: '{shadows.md}',
    duration: '200ms',
    easing: 'ease-out',
  },

  // 포커스 링
  focusRing: {
    ringWidth: '2px',
    ringColor: '{colors.ring}',
    ringOffset: '2px',
    transition: '150ms ease-out',
  },

  // 토글 전환
  toggle: {
    duration: '200ms',
    easing: 'spring(1, 100, 10, 0)',
  },

  // 체크박스 체크
  checkbox: {
    scale: [1, 1.1, 1],
    duration: '200ms',
  },

  // 스위치 슬라이드
  switch: {
    translateX: '100%',
    duration: '200ms',
    easing: 'ease-out',
  },

  // 리플 효과
  ripple: {
    scale: [0, 2],
    opacity: [0.3, 0],
    duration: '400ms',
  },
} as const

Tailwind 인터랙션 클래스

// tailwind.config.ts

export const interactionClasses = {
  // 기본 인터랙션
  'interactive-base': 'transition-colors duration-200 ease-out',

  // 버튼 인터랙션
  'interactive-button': [
    'transition-all duration-200 ease-out',
    'hover:shadow-sm hover:-translate-y-0.5',
    'active:translate-y-0 active:shadow-none',
    'focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2',
  ].join(' '),

  // 카드 인터랙션 (클릭 가능)
  'interactive-card': [
    'transition-all duration-200 ease-out',
    'hover:shadow-md hover:-translate-y-1',
    'active:shadow-sm active:translate-y-0',
  ].join(' '),

  // 링크 인터랙션
  'interactive-link': [
    'transition-colors duration-150',
    'hover:text-primary',
    'focus-visible:outline-2 focus-visible:outline-ring',
  ].join(' '),

  // 입력 인터랙션
  'interactive-input': [
    'transition-colors duration-150',
    'hover:border-border-hover',
    'focus:border-ring focus:ring-1 focus:ring-ring',
  ].join(' '),
}

애니메이션 토큰화

Duration 스케일

// tokens/motion.ts

export const duration = {
  /** 즉각 피드백 (hover, focus) */
  instant: '50ms',
  /** 빠른 전환 (색상 변경) */
  fast: '100ms',
  /** 일반 전환 (대부분의 UI) */
  normal: '200ms',
  /** 느린 전환 (모달 열기) */
  slow: '300ms',
  /** 복잡한 애니메이션 */
  slower: '400ms',
  /** 페이지 전환 */
  slowest: '500ms',
} as const

Easing 함수

// tokens/motion.ts

export const easing = {
  /** 표준 감속 - 요소 진입 */
  easeOut: 'cubic-bezier(0.0, 0.0, 0.2, 1)',
  /** 표준 가속 - 요소 퇴장 */
  easeIn: 'cubic-bezier(0.4, 0.0, 1, 1)',
  /** 표준 가감속 - 상태 전환 */
  easeInOut: 'cubic-bezier(0.4, 0.0, 0.2, 1)',
  /** 강조 감속 - 주목 필요 */
  emphasizedDecelerate: 'cubic-bezier(0.05, 0.7, 0.1, 1.0)',
  /** 스프링 - 자연스러운 동작 */
  spring: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
  /** 바운스 */
  bounce: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
} as const

애니메이션 프리셋

// tokens/animations.ts

export const animations = {
  // 페이드 인
  fadeIn: {
    from: { opacity: 0 },
    to: { opacity: 1 },
    duration: '{duration.normal}',
    easing: '{easing.easeOut}',
  },

  // 슬라이드 업
  slideUp: {
    from: { opacity: 0, transform: 'translateY(10px)' },
    to: { opacity: 1, transform: 'translateY(0)' },
    duration: '{duration.normal}',
    easing: '{easing.easeOut}',
  },

  // 스케일 인
  scaleIn: {
    from: { opacity: 0, transform: 'scale(0.95)' },
    to: { opacity: 1, transform: 'scale(1)' },
    duration: '{duration.fast}',
    easing: '{easing.spring}',
  },

  // 스핀
  spin: {
    from: { transform: 'rotate(0deg)' },
    to: { transform: 'rotate(360deg)' },
    duration: '1s',
    easing: 'linear',
    iterationCount: 'infinite',
  },

  // 펄스
  pulse: {
    '0%, 100%': { opacity: 1 },
    '50%': { opacity: 0.5 },
    duration: '2s',
    easing: '{easing.easeInOut}',
    iterationCount: 'infinite',
  },

  // 바운스
  bounce: {
    '0%, 100%': { transform: 'translateY(0)' },
    '50%': { transform: 'translateY(-10px)' },
    duration: '1s',
    easing: '{easing.bounce}',
    iterationCount: 'infinite',
  },
} as const

CSS 애니메이션 출력

/* dist/animations.css */

@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes slideUp {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes scaleIn {
  from {
    opacity: 0;
    transform: scale(0.95);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

.animate-fade-in {
  animation: fadeIn 200ms var(--ease-out) forwards;
}

.animate-slide-up {
  animation: slideUp 200ms var(--ease-out) forwards;
}

.animate-scale-in {
  animation: scaleIn 100ms var(--spring) forwards;
}

Reduced Motion 지원

// hooks/use-reduced-motion.ts

export function useReducedMotion(): boolean {
  const [reducedMotion, setReducedMotion] = useState(false)

  useEffect(() => {
    const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
    setReducedMotion(mediaQuery.matches)

    const handler = (e: MediaQueryListEvent) => setReducedMotion(e.matches)
    mediaQuery.addEventListener('change', handler)
    return () => mediaQuery.removeEventListener('change', handler)
  }, [])

  return reducedMotion
}

// 사용 예시
function AnimatedComponent() {
  const reducedMotion = useReducedMotion()

  return (
    <motion.div
      initial={{ opacity: 0, y: reducedMotion ? 0 : 10 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{
        duration: reducedMotion ? 0 : 0.2,
      }}
    >
      Content
    </motion.div>
  )
}
/* CSS 레벨 지원 */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

체크리스트

참고 자료

  • Material Design Motion
  • Framer Motion
  • CSS Easing Functions
  • prefers-reduced-motion MDN

관련 문서

토큰 아키텍처

Primitive·Semantic·Component 3계층 토큰 구조와 AI 친화 네이밍, W3C DTCG 1.0 포맷·Style Dictionary v5 변환 파이프라인, MCP 서버 연동으로 AI가 토큰을 추론·조합하게 만드는 설계

Next.js 프로젝트 생성

Windows 바이브코딩 초기 세팅 · create-next-app 최신 기본값으로 첫 Next.js 프로젝트를 만들고 실행합니다

접근성 내장

AI가 자동으로 접근성을 갖춘 컴포넌트를 생성하도록 설계하기

일관성 패턴

12컬럼 그리드, 4px 기반 스페이싱 스케일, mobile-first 반응형 규칙과 layout linting·consistency score로 AI 생성 UI의 일관성을 보장하는 방법.

폼 & 데이터 입력

FormField 합성, Zod 검증, onBlur 검증 타이밍, 에러 메시지·마이크로카피 표준화, 다단계·동적·조건부 폼 등 AI가 일관된 폼을 생성하기 위한 패턴.

On this page

인터랙션 계층상태 피드백 패턴상태 전이 다이어그램상태별 스타일 토큰피드백 컴포넌트마이크로인터랙션인터랙션 유형마이크로인터랙션 토큰Tailwind 인터랙션 클래스애니메이션 토큰화Duration 스케일Easing 함수애니메이션 프리셋CSS 애니메이션 출력Reduced Motion 지원체크리스트상태 피드백마이크로인터랙션애니메이션 토큰접근성참고 자료