Files
2nd/10_Wiki/Topics/Architecture/Micro-interactions.md
T
Antigravity Agent f8b21af4be Wiki cleanup: error-doc removal, dedup merge, link normalization
10_Wiki/Topics 대규모 정리:
- 오류 캡처/미완성 stub 문서 227개 제거
- 교차폴더 중복 43클러스터 병합 (63파일 → redirect)
- 링크명 정규화: 깨진 링크 수정·redirect 직결·개념 매핑 ~2,400건
- 카테고리 MOC 6개 신규 생성
- Graph 섹션 미해결 related-keyword 링크 10,058건 제거

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 23:52:15 +09:00

7.6 KiB

id, title, category, status, canonical_id, aliases, duplicate_of, source_trust_level, confidence_score, verification_status, tags, raw_sources, last_reinforced, github_commit, tech_stack
id title category status canonical_id aliases duplicate_of source_trust_level confidence_score verification_status tags raw_sources last_reinforced github_commit tech_stack
wiki-2026-0508-micro-interactions Micro-interactions 10_Wiki/Topics verified self
Microinteractions
UI Micro-animations
Tiny UX
none A 0.9 applied
ux
ui
animation
design
frontend
2026-05-10 pending
language framework
typescript react-framer-motion

Micro-interactions

매 한 줄

"매 작은 순간이 매 product 의 personality 를 결정한다.". Micro-interaction 은 매 single task 를 중심으로 한 매 작은 UX moment — toggle, like, error feedback, hover state. Dan Saffer 의 매 4-part model (Trigger / Rules / Feedback / Loops & Modes) 이 표준. 매 well-crafted 매 micro-interaction 은 매 product 를 매 functional → 매 delightful 로 매 끌어올림.

매 핵심

매 Saffer's 4 parts

  1. Trigger: user 가 시작 (click) 또는 system (notification).
  2. Rules: 매 무엇이 일어나는지 (state transition).
  3. Feedback: 매 user 에게 결과 알림 (visual / sound / haptic).
  4. Loops & Modes: 매 시간에 따른 변화, 매 special state.

매 언제 사용

  • Status communication: loading, saving, online/offline.
  • Affordance hint: hover, focus, disabled.
  • Error prevention: input validation 즉시 feedback.
  • Reward: like animation, achievement unlock.
  • Branding: 매 unique transition 으로 매 personality.

매 design 원칙

  • 빠르게: 매 100-300ms — 매 너무 길면 매 friction.
  • Purposeful: 매 deco 아닌 매 information 전달.
  • Consistent: 매 동일 action → 매 동일 feedback.
  • Respectful: prefers-reduced-motion 존중.
  • Subtle by default: 매 hero animation 은 매 rare.

매 performance

  • 매 transform/opacity 만 사용 (compositor only — GPU).
  • 매 layout/paint trigger 회피 (width, height, top, left).
  • 매 will-change 의 sparing 사용.

💻 패턴

CSS toggle (transform only)

.toggle {
  width: 44px; height: 24px;
  background: #ccc;
  border-radius: 12px;
  transition: background 200ms ease;
  position: relative;
  cursor: pointer;
}
.toggle::after {
  content: '';
  position: absolute;
  top: 2px; left: 2px;
  width: 20px; height: 20px;
  background: white;
  border-radius: 50%;
  transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1);
}
.toggle.on { background: #4ade80; }
.toggle.on::after { transform: translateX(20px); }

@media (prefers-reduced-motion: reduce) {
  .toggle, .toggle::after { transition: none; }
}

Framer Motion (React)

import { motion, AnimatePresence } from 'framer-motion';

function LikeButton({ liked, onToggle }: { liked: boolean; onToggle: () => void }) {
  return (
    <motion.button
      whileTap={{ scale: 0.9 }}
      whileHover={{ scale: 1.05 }}
      onClick={onToggle}
      aria-pressed={liked}
    >
      <AnimatePresence mode="wait">
        <motion.span
          key={liked ? 'on' : 'off'}
          initial={{ scale: 0.5, opacity: 0 }}
          animate={{ scale: 1, opacity: 1 }}
          exit={{ scale: 0.5, opacity: 0 }}
          transition={{ duration: 0.15 }}
        >
          {liked ? '❤️' : '🤍'}
        </motion.span>
      </AnimatePresence>
    </motion.button>
  );
}

Skeleton loading

const Skeleton = () => (
  <motion.div
    className="h-4 bg-gray-200 rounded"
    animate={{ opacity: [0.5, 1, 0.5] }}
    transition={{ duration: 1.5, repeat: Infinity, ease: 'easeInOut' }}
  />
);

Optimistic UI (form submit)

function CommentForm({ onSubmit }: { onSubmit: (text: string) => Promise<void> }) {
  const [pending, setPending] = useState(false);
  const [error, setError] = useState<string | null>(null);

  return (
    <form onSubmit={async (e) => {
      e.preventDefault();
      const text = (e.target as any).text.value;
      setPending(true);
      setError(null);
      try {
        await onSubmit(text);
      } catch (err) {
        setError('Failed. Try again.');
      } finally {
        setPending(false);
      }
    }}>
      <input name="text" disabled={pending} />
      <motion.button
        whileTap={{ scale: 0.95 }}
        animate={pending ? { opacity: 0.6 } : { opacity: 1 }}
        disabled={pending}
      >
        {pending ? 'Posting…' : 'Post'}
      </motion.button>
      <AnimatePresence>
        {error && (
          <motion.div
            initial={{ opacity: 0, y: -4 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0 }}
            role="alert"
          >{error}</motion.div>
        )}
      </AnimatePresence>
    </form>
  );
}

Inline validation

function EmailInput() {
  const [value, setValue] = useState('');
  const [touched, setTouched] = useState(false);
  const valid = /\S+@\S+\.\S+/.test(value);
  const showError = touched && !valid && value.length > 0;

  return (
    <div>
      <input
        type="email"
        value={value}
        onChange={(e) => setValue(e.target.value)}
        onBlur={() => setTouched(true)}
        aria-invalid={showError}
      />
      <AnimatePresence>
        {showError && (
          <motion.span
            initial={{ opacity: 0, height: 0 }}
            animate={{ opacity: 1, height: 'auto' }}
            exit={{ opacity: 0, height: 0 }}
            className="text-red-600 text-sm"
            role="alert"
          >Invalid email</motion.span>
        )}
      </AnimatePresence>
    </div>
  );
}

Pull-to-refresh (mobile)

import { motion, useMotionValue, useTransform } from 'framer-motion';

function PullToRefresh({ onRefresh }: { onRefresh: () => Promise<void> }) {
  const y = useMotionValue(0);
  const opacity = useTransform(y, [0, 80], [0, 1]);
  const rotate = useTransform(y, [0, 80], [0, 360]);

  return (
    <motion.div
      drag="y"
      dragConstraints={{ top: 0, bottom: 100 }}
      dragElastic={0.3}
      onDragEnd={async (_, info) => {
        if (info.offset.y > 80) await onRefresh();
        y.set(0);
      }}
      style={{ y }}
    >
      <motion.div style={{ opacity, rotate }}></motion.div>
      {/* content */}
    </motion.div>
  );
}

매 결정 기준

Element Duration Easing
Toggle, hover 100-200ms ease-out
Modal enter 200-300ms cubic-bezier(0.4, 0, 0.2, 1)
Modal exit 150-200ms cubic-bezier(0.4, 0, 1, 1)
Page transition 300-500ms ease-in-out
Loading shimmer 1500ms loop ease-in-out

기본값: 매 200ms + ease-out + transform/opacity. 매 prefers-reduced-motion 매 항상 존중.

🔗 Graph

🤖 LLM 활용

언제: UI feedback 설계, button/toggle/input 의 polish, error/loading/empty state 의 personality. 언제 X: 매 dense data table, 매 power-user tool — 매 animation 이 매 friction.

안티패턴

  • 너무 긴 duration: 매 500ms+ → 매 sluggish.
  • No reduced-motion: 매 vestibular disorder user 에게 매 hostile.
  • Decorative only: 매 information 없는 매 animation → 매 cognitive load.
  • Inconsistent: 매 같은 action 이 매 다른 feedback.
  • Layout-trigger animation: 매 width/height/top → 매 jank.

🧪 검증 / 중복

  • Verified (Saffer "Microinteractions", Material Design motion guidelines, Apple HIG, Framer Motion docs, web.dev animations).
  • 신뢰도 A.

🕓 Changelog

날짜 변경
2026-05-08 Phase 1
2026-05-10 Manual cleanup — Saffer 4-part + Framer Motion / a11y 패턴