Communitygithub.com

Motion 고급

React / Next.js에서 드래그 앤 드롭, 제스처, 텍스트 애니메이션, SVG 경로 그리기, 커스텀 훅, 명령형 시퀀스(useAnimate), 로더 등 고급 모션 패턴을 구현합니다. motion-foundations가 필수적으로 설정되어 있어야 합니다. 드래그 앤 드롭, 제스처, 텍스트 또는 SVG 애니메이션, React 또는 Next.js에서 명령형 애니메이션 시퀀스를 구축할 때 사용하세요.

Motion 고급란 무엇인가요?

Motion-advanced는 React/Next.js 애플리케이션에서 복잡하고 상호작용적이며 물리 기반의 애니메이션 패턴을 구현하기 위한 고급 AI 에이전트 스킬입니다. 첫 번째 단계로 `motion-foundations`를 설정한 후 사용해야 하며, 기본 `motion-patterns`로 충분하지 않을 때 활성화됩니다. 주요 기능으로는 드래그 앤 드롭 상호작용(드래그 해제 시트, 스와이프 감지, 재정렬 가능 목록), 제스처 훅(롱 프레스), 텍스트 애니메이션(단어별 등장, 타이핑 효과, 숫자 카운터), SVG 애니메이션(경로 그리기, 아이콘 변형, 원형 진행률 표시줄), 커스텀 훅(useScrollReveal, 커서 따라가기), 그리고 `useAnimate`를 활용한 명령형 시퀀스를 포함합니다. 이 스킬은 물리 기반 모션(`useSpring`, `springs.*`)을 권장하며, `useMotionValue`와 `useTransform`을 사용하여 불필요한 리렌더링을 방지하고, 모든 무한 애니메이션은 탭이 백그라운드에 있을 때 일시 중지되어야 합니다. 드래그 상호작용은 반드시 터치 기기에서 테스트되어야 하며, 스와이프 임계값은 오프셋과 속도를 결합하여 명시적으로 설정하고, 모든 토큰 값은 `motion-foundations`에서 가져옵니다. 커스텀 훅은 클린업을 처리해야 하며, SVG 변형은 동일한 경로 명령 개수가 필요합니다.

지원 대상~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/motion-advanced

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Motion 고급은(는) 무엇을 하나요?

복잡하고 상호작용적이며 물리 기반의 애니메이션 패턴. 먼저 motion-foundations가 설정되어 있어야 합니다. motion-patterns로 충분하지 않을 때 사용하세요.

활성화 시점

  • 드래그 해제 시트, 스와이프 제스처 또는 재정렬 가능 목록을 구축할 때
  • 텍스트를 단어별, 문자별로 애니메이션하거나 라이브 카운터로 사용할 때
  • SVG 경로를 그리고, 아이콘을 변형하거나, 원형 진행률을 애니메이션할 때
  • 커스텀 애니메이션 훅(useScrollReveal, 마그네틱 버튼, 커서 따라가기)을 작성할 때
  • useAnimate로 멀티스텝 애니메이션을 명령형으로 시퀀싱할 때
  • 스피너, 샤이머 스켈레톤, 펄스 표시기 또는 로딩 버튼 상태를 구축할 때

산출물

이 스킬은 다음을 생성합니다:

  • 드래그 상호작용: 드래그 가능 카드, 드래그 해제 시트, Reorder.Group 목록
  • 제스처 훅: 스와이프 감지, 롱 프레스, 핀치 개요
  • 텍스트 애니메이션 컴포넌트: 단어 등장, 문자 타자기, 숫자 카운터
  • SVG 애니메이션: 경로 그리기, 아이콘 변형, 스트로크 진행률 링
  • 커스텀 훅: useScrollReveal, useHoverScale, useNavigationDirection, useInViewOnce
  • useAnimate를 활용한 명령형 시퀀스 (인터럽트 안전 async/await)
  • 로더 컴포넌트: 스피너, 샤이머, 펄스 도트, 진행 바, 버튼 로딩 상태

원칙

  • 물리 기반 모션(useSpring, springs.*)은 직접 조작 시 항상 지속 시간 기반보다 더 자연스럽습니다.
  • useMotionValue + useTransform은 리렌더링을 유발하지 않고 파생 값을 계산합니다.
  • useAnimate 시퀀스는 명령형이며 인터럽트 안전합니다. 중간에 animate()를 호출하면 이전 애니메이션이 자동으로 취소됩니다.
  • 모션 값(useMotionValue, useSpring)은 SSR 안전하며 수화 오류를 일으키지 않습니다.

규칙

  1. 드래그 상호작용은 마우스뿐만 아니라 터치 기기에서도 테스트되어야 합니다. drag 속성은 둘 다에서 작동하지만 느낌과 임계값이 다릅니다.
  2. 무한 애니메이션은 document.visibilityState === "hidden"일 때 일시 중지되어야 합니다. 백그라운드 탭은 GPU/CPU를 소비해서는 안 됩니다.
  3. 스와이프 임계값은 명시적이어야 합니다. 속도만으로 의도를 추론하지 말고 offset + velocity 검사를 결합하세요.
  4. useAnimate 스코프 참조는 마운트된 DOM 요소에 연결되어야 합니다. 마운트 전에 animate()를 호출하면 조용히 실패합니다.
  5. 모션 값은 렌더링 시 재생성되어서는 안 됩니다. 컴포넌트 본문 내부의 useMotionValue(0)는 올바르지만, 렌더링에서 new MotionValue(0)는 올바르지 않습니다.
  6. 모든 토큰 값은 motion-foundations에서 가져옵니다. 인라인 숫자는 사용하지 않습니다.
  7. 커스텀 훅은 클린업을 처리해야 합니다. 모든 window.addEventListener에는 useEffect 반환문에 일치하는 removeEventListener가 필요합니다.
  8. SVG 변형에는 동일한 경로 명령 개수가 필요합니다. 명령 구조가 다른 경로는 보간 대신 스냅됩니다.

결정 가이드

올바른 고급 API 선택하기

시나리오API
릴리스 시 물리 기반 드래그drag + dragTransition: springs.release
순서가 있는 드래그 재정렬 목록Reorder.Group + Reorder.Item
드래그 오프셋으로 해제drag="y" + onDragEnd 오프셋 확인
왼쪽/오른쪽 스와이프drag="x" + onDragEnd 오프셋 확인
롱 프레스useLongPress 훅
시간 경과에 따라 부드러워진 값useSpring
다른 값에서 파생된 값useTransform
멀티스텝 시퀀스useAnimate + async/await
원샷 명령형 애니메이션motion의 animate()
단어별로 등장하는 텍스트inline-block 범위에 스태거
SVG 그리기pathLength 0 → 1
SVG 변형d 속성 트윈 (동일한 명령)
원형 진행률strokeDashoffset 트윈

useSpring vs 스프링 전환 사용 시기

useSpringtransition: springs.*
사용처커서 따라가기, 포인터 추적 값개별 상태 전환
업데이트연속적, 모든 프레임상태 변경에 의해 트리거됨
인터럽트부드러움 — 물리가 속도에서 다시 시작현재 값에서 다시 시작

핵심 개념

useMotionValue + useTransform

리렌더링 없는 반응형 계산:

const x = useMotionValue(0)
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0])
// opacity는 x가 변경될 때마다 모든 프레임에서 업데이트됩니다 — setState, 리렌더링 없음

useAnimate

[scope, animate]를 반환합니다. 스코프 참조는 DOM 요소에 연결되어야 합니다. animate() 호출은 인터럽트 안전합니다. 중간에 호출하면 이전 실행이 취소됩니다.

const [scope, animate] = useAnimate()

async function play() {
  await animate(".step-1", { opacity: 1 }, { duration: 0.3 })
  await animate(".step-2", { x: 0 },       { duration: 0.4 })
        animate(".step-3", { scale: 1 },    { duration: 0.25 })  // 파이어 앤 포겟
}

return <div ref={scope}>...</div>

코드 예제

드래그 가능 카드

"use client"
import { motion } from "motion/react"
import { springs, motionTokens } from "@/lib/motion-tokens"

<motion.div
  drag
  dragConstraints={{ left: -100, right: 100, top: -100, bottom: 100 }}
  dragElastic={0.1}
  whileDrag={{
    scale: motionTokens.scale.pop,
    boxShadow: "0 16px 40px rgba(0,0,0,0.2)",
  }}
  dragTransition={springs.release}
/>

드래그 해제 시트

"use client"
import { motion, useMotionValue, useTransform } from "motion/react"

export function BottomSheet({ onClose }: { onClose: () => void }) {
  const y = useMotionValue(0)
  const opacity = useTransform(y, [0, 200], [1, 0])

  return (
    <motion.div
      drag="y"
      dragConstraints={{ top: 0 }}
      style={{ y, opacity }}
      onDragEnd={(_, info) => {
        // 규칙 3: offset + velocity 결합
        if (info.offset.y > 120 || info.velocity.y > 500) onClose()
      }}
    />
  )
}

재정렬 가능 목록

"use client"
import { Reorder } from "motion/react"

export function SortableList() {
  const [items, setItems] = useState(initialItems)
  return (
    <Reorder.Group axis="y" values={items} onReorder={setItems}>
      {items.map((item) => (
        <Reorder.Item key={item.id} value={item}>
          {item.label}
        </Reorder.Item>
      ))}
    </Reorder.Group>
  )
}

스와이프 감지

"use client"
import { motion } from "motion/react"

const OFFSET_THRESHOLD  = 50
const VELOCITY_THRESHOLD = 300

<motion.div
  drag="x"
  dragConstraints={{ left: 0, right: 0 }}
  onDragEnd={(_, info) => {
    const swipedRight = info.offset.x > OFFSET_THRESHOLD  || info.velocity.x > VELOCITY_THRESHOLD
    const swipedLeft  = info.offset.x < -OFFSET_THRESHOLD || info.velocity.x < -VELOCITY_THRESHOLD
    if (swipedRight) onSwipeRight()
    if (swipedLeft)  onSwipeLeft()
  }}
/>

롱 프레스 훅

import { useRef } from "react"

export function useLongPress(callback: () => void, ms = 600) {
  const timerRef = useRef<ReturnType<typeof setTimeout>>()
  return {
    onPointerDown:  () => { timerRef.current = setTimeout(callback, ms) },
    onPointerUp:    () => clearTimeout(timerRef.current),
    onPointerLeave: () => clearTimeout(timerRef.current),
  }
}

단어별 등장

"use client"
import { motion } from "motion/react"
import { springs } from "@/lib/motion-tokens"

export function AnimatedText({ text }: { text: string }) {
  return (
    <motion.p
      variants={{ visible: { transition: { staggerChildren: 0.05 } } }}
      initial="hidden"
      animate="visible"
    >
      {text.split(" ").map((word, i) => (
        <motion.span
          key={i}
          className="inline-block mr-1"
          variants={{
            hidden:  { opacity: 0, y: 12 },
            visible: { opacity: 1, y: 0, transition: springs.gentle },
          }}
        >
          {word}
        </motion.span>
      ))}
    </motion.p>
  )
}

숫자 카운터

"use client"
import { useRef, useEffect } from "react"
import { animate } from "motion"
import { motionTokens } from "@/lib/motion-tokens"

export function Counter({ to }: { to: number }) {
  const nodeRef = useRef<HTMLSpanElement>(null)

  useEffect(() => {
    const controls = animate(0, to, {
      duration: motionTokens.duration.crawl,
      ease: motionTokens.easing.smooth,
      onUpdate: (v) => {
        if (nodeRef.current) nodeRef.current.textContent = Math.round(v).toString()
      },
    })
    return controls.stop   // 규칙 7: 클린업
  }, [to])

  return <span ref={nodeRef} />
}

SVG 경로 그리기

"use client"
import { motion } from "motion/react"
import { motionTokens } from "@/lib/motion-tokens"

<motion.path
  d="M 0 100 Q 50 0 100 100"
  initial={{ pathLength: 0, opacity: 0 }}
  animate={{ pathLength: 1, opacity: 1 }}
  transition={{ duration: motionTokens.duration.slow, ease: motionTokens.easing.smooth }}
/>

스트로크 진행률 링

"use client"
import { motion } from "motion/react"
import { motionTokens } from "@/lib/motion-tokens"

const CIRCUMFERENCE = 2 * Math.PI * 40   // r=40

export function ProgressRing({ progress }: { progress: number }) {
  return (
    <svg width="100" height="100" viewBox="0 0 100 100">
      <circle cx="50" cy="50" r="40" fill="none" stroke="#e5e7eb" strokeWidth="8" />
      <motion.circle
        cx="50" cy="50" r="40"
        fill="none" stroke="#6366f1" strokeWidth="8"
        strokeLinecap="round"
        strokeDasharray={CIRCUMFERENCE}
        animate={{ strokeDashoffset: CIRCUMFERENCE - (progress / 100) * CIRCUMFERENCE }}
        transition={{ duration: motionTokens.duration.normal, ease: motionTokens.easing.smooth }}
        style={{ rotate: -90, transformOrigin: "center" }}
      />
    </svg>
  )
}

useScrollReveal 훅

"use client"
import { useRef } from "react"
import { useScroll, useTransform } from "motion/react"
import { motionTokens } from "@/lib/motion-tokens"

export function useScrollReveal() {
  const ref = useRef(null)
  const { scrollYProgress } = useScroll({ target: ref, offset: ["start end", "end start"] })
  const opacity = useTransform(scrollYProgress, [0, 0.3], [0, 1])
  const y       = useTransform(scrollYProgress, [0, 0.3], [motionTokens.distance.lg, 0])
  return { ref, style: { opacity, y } }
}

// 사용법
const { ref, style } = useScrollReveal()
<motion.section ref={ref} style={style} />

커서 따라가기

"use client"
import { useEffect } from "react"
import { motion, useMotionValue, useSpring } from "motion/react"
import { springs } from "@/lib/motion-tokens"

export function CursorFollower() {
  const x = useMotionValue(-100)
  const y = useMotionValue(-100)
  const sx = useSpring(x, springs.gentle)
  const sy = useSpring(y, springs.gentle)

  useEffect(() => {
    const move = (e: MouseEvent) => { x.set(e.clientX); y.set(e.clientY) }
    window.addEventListener("mousemove", move)
    return () => window.removeEventListener("mousemove", move)   // 규칙 7
  }, [])

  return (
    <motion.div
      className="fixed top-0 left-0 w-6 h-6 rounded-full bg-indigo-500
                 pointer-events-none -translate-x-1/2 -translate-y-1/2 z-50"
      style={{ x: sx, y: sy }}
    />
  )
}

샤이머 스켈레톤

"use client"
import { useEffect } from "react"
import { motion, useAnimation } from "motion/react"
import { motionTokens } from "@/lib/motion-tokens"

export function ShimmerSkeleton({ className = "" }: { className?: string }) {
  const controls = useAnimation()

  useEffect(() => {
    const play = () =>
      controls.start({
        x: ["-100%", "100%"],
        transition: {
          repeat: Infinity,
          duration: motionTokens.duration.crawl,
          ease: motionTokens.easing.linear,
        },
      })

    const handleVisibility = () => {
      if (document.visibilityState === "hidden") controls.stop()
      else void play()
    }

    void play()
    document.addEventListener("visibilitychange", handleVisibility)
    return () => {
      controls.stop()
      document.removeEventListener("visibilitychange", handleVisibility)
    }
  }, [controls])

  return (
    <div className={`relative overflow-hidden bg-gray-200 rounded ${className}`}>
      <motion.div
        className="absolute inset-0 bg-gradient-to-r from-transparent via-white/60 to-transparent"
        initial={{ x: "-100%" }}
        animate={controls}
      />
    </div>
  )
}

버튼 로딩 상태

"use client"
import { motion, AnimatePresence } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"

export function LoadingButton({
  loading,
  label,
  onClick,
}: {
  loading: boolean
  label: string
  onClick: () => void
}) {
  return (
    <motion.button
      onClick={onClick}
      animate={{ opacity: loading ? 0.7 : 1 }}
      whileTap={loading ? {} : { scale: motionTokens.scale.press }}
      transition={springs.snappy}
      disabled={loading}
    >
      <AnimatePresence mode="wait">
        {loading ? (
          <motion.span
            key="loading"
            initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
            transition={{ duration: motionTokens.duration.fast }}
          >
            …
          </motion.span>
        ) : (
          <motion.span
            key="label"
            initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
            transition={{ duration: motionTokens.duration.fast }}
          >
            {label}
          </motion.span>
        )}
      </AnimatePresence>
    </motion.button>
  )
}

가시성 일시 중지가 있는 무한 애니메이션

"use client"
import { useEffect } from "react"
import { motion, useAnimation } from "motion/react"
import { motionTokens } from "@/lib/motion-tokens"

export function PulseDot() {
  const controls = useAnimation()

  useEffect(() => {
    const pulse = () =>
      controls.start({
        scale: [1, 1.4, 1],
        opacity: [1, 0.6, 1],
        transition: { repeat: Infinity, duration: motionTokens.duration.crawl },
      })

    // 규칙 2: 탭이 숨겨지면 일시 중지
    const handleVisibility = () => {
      if (document.visibilityState === "hidden") controls.stop()
      else void pulse()
    }

    void pulse()
    document.addEventListener("visibilitychange", handleVisibility)
    return () => {
      controls.stop()
      document.removeEventListener("visibilitychange", handleVisibility)
    }
  }, [controls])

  return (
    <motion.div animate={controls} className="w-3 h-3 rounded-full bg-indigo-500" />
  )
}

SVG 아이콘 변형 (모프)

"use client"
import { useState } from "react"
import { motion } from "motion/react"
import { motionTokens } from "@/lib/motion-tokens"

// 경로는 동일한 수의 명령을 가져야 합니다. d 속성 트윈은 인덱스별로 일치합니다.
const menuPath = "M 3 5 h 18 M 3 12 h 18 M 3 19 h 18"          // 햄버거 메뉴 (3개 명령)
const closePath = "M 5 5 l 14 14 M 19 5 l -14 14"               // X 닫기 (2개 명령) → 부적합
// 대신 동일한 명령 구조로 구성:
const closePathFixed = "M 4 4 L 20 20 M 20 4 L 4 20"           // 2개 명령

export function MorphIcon() {
  const [isOpen, setIsOpen] = useState(false)
  return (
    <motion.svg
      width="24" height="24" viewBox="0 0 24 24"
      stroke="currentColor" strokeWidth="2" fill="none"
      style={{ cursor: "pointer" }}
      onClick={() => setIsOpen(!isOpen)}
    >
      <motion.path
        d={isOpen ? closePathFixed : menuPath}
        animate={{ d: isOpen ? closePathFixed : menuPath }}
        transition={{ duration: motionTokens.duration.normal, ease: motionTokens.easing.smooth }}
      />
    </motion.svg>
  )
}

결론

Motion Advanced는 motion-foundations 위에 구축된 복잡하고 물리 기반이며 상호작용적인 애니메이션을 위한 최종 레이어입니다. 드래그 앤 드롭, 제스처, 텍스트 효과, SVG 모션, 커스텀 훅, 명령형 시퀀스를 다룹니다. 기본 API인 drag, Reorder, useAnimate, useSpring 및 useTransform 간의 전환 방법을 이해하면 견고하고 성능이 뛰어난 직관적인 애니메이션을 구축할 수 있습니다.

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

관련 스킬