Communitygithub.com

joo6077/react-animation

React 애니메이션을 라이브러리 없이 Tailwind CSS, View Transitions API, Pointer Events로 구현하는 AI 에이전트 스킬입니다.

react-animation 是什么?

이 스킬은 React 프로젝트에서 애니메이션을 구현할 때 외부 라이브러리 사용을 금지하고, Tailwind CSS 유틸리티, View Transitions API, Pointer Events API를 활용하여 직접 구현하도록 설계되었습니다. '라이브러리 0개 원칙'을 엄격히 적용하여 framer-motion, react-spring 등 인기 애니메이션 라이브러리의 설치와 import를 차단합니다. 요청된 애니메이션의 복잡도에 따라 T1(Tailwind+CSS), T2(View Transitions), T3(Pointer Events+FSM) 세 가지 티어로 자동 분류하여 가장 낮은 티어부터 적용합니다. 주요 특징으로는 `prefers-reduced-motion` 접근성 준수, 브라우저 호환성 고려, 증거 기반 검증 프로토콜 요구사항 등이 포함되어 있습니다. 이 스킬은 Claude Code 플러그인 환경에서 동작하며, AI 에이전트가 애니메이션 코드를 생성할 때 정해진 규칙과 프로세스를 따르도록 안내합니다.

兼容平台✓Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/joo6077/claude-plugins/tree/HEAD/react-kit/skills/react-animation

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

React Animation

설치본 플러그인에는 docs/react/ 가 없다 — 이 파일의 docs/... 경로를 열 수 없으면 https://raw.githubusercontent.com/joo6077/claude-plugins/main/ 뒤에 같은 경로를 붙여 읽고, 그래도 못 읽으면 내용을 지어내지 말고 못 읽었다고 적는다.

주의사항

  1. 라이브러리 0개 원칙 — 절대 예외 없음: Motion(framer-motion) / dnd-kit / react-spring / react-transition-group / @formkit/auto-animate / react-dnd / gsap / lottie-react / react-beautiful-dnd / animate.css 는 설치 및 import 금지. 이들의 import 구문이 코드베이스에 존재하면 /react-audit 이 빌드 실패를 발행한다. 사용자가 요청하더라도 대안 구현을 제시하고 라이브러리 사용을 거부한다.

  2. 가장 낮은 Tier부터 시도: "animation" 요청을 받으면 무조건 Tier 3부터 쓰는 실수가 잦다. 판정 규칙을 따라 Tier 1로 해결 가능한지 먼저 확인하고, 불가능할 때만 더 높은 Tier로 올라간다.

  3. tailwindcss-animate 플러그인 확인: animate-in, fade-in-*, slide-in-from-* 같은 확장 유틸리티는 tailwindcss-animate 가 있어야 동작한다. shadcn 설치 프로젝트에는 기본 포함되지만, 없으면 pnpm add -D tailwindcss-animate 후 tailwind.config.ts 에 플러그인 등록을 안내한다.

  4. view-transition-name 고유성: 같은 이름을 두 요소가 동시에 가지면 "duplicate name" 오류가 발생한다. 반드시 동적 id (view-transition-name: task-${item.id})를 사용해 고유성을 보장한다.

  5. Pointer cancel 처리 필수: pointercancel 이벤트를 처리하지 않으면 탭 전환, 다른 앱 포커스, 요소 unmount 시 드래그 상태가 leak된다. onPointerCancel 핸들러에서 반드시 FSM을 idle로 리셋한다.

  6. touch-action: none 빠뜨리면 모바일 드래그 불가: pointer event 기반 드래그 요소에 Tailwind touch-none 클래스를 반드시 추가해야 브라우저 기본 스크롤/줌이 차단된다.

  7. Zustand drag store는 전역 단일 인스턴스: feature마다 별도 drag store를 만들지 않는다. src/presentation/shared/stores/drag-store.ts 하나를 모든 feature가 공유한다.

  8. prefers-reduced-motion 미적용: 모든 Tier에서 @media (prefers-reduced-motion: reduce) 또는 Tailwind motion-reduce:* variant로 애니메이션을 비활성화해야 한다. /react-audit 이 누락 시 경고를 발행한다.

  9. 애니메이션은 presentation 전용: domain/, data/ 레이어에 pointer event / view transition / Zustand drag store를 import하지 않는다. 애니메이션 관련 코드는 src/presentation/ 에만 위치한다.

  10. Firefox types 파라미터 미지원: document.startViewTransition({ update, types: [...] }) 의 types 옵션은 2026년 기준 Chromium 계열에서만 안정적으로 지원된다. withViewTransition 래퍼에서 try/catch로 fallback을 처리한다.

  11. scroll-driven animation 브라우저 지원 (2026-08-13 갱신): animation-timeline: scroll() / scroll-timeline 은 Can I Use 기준 전역 85.43% — Chrome/Edge 115+, Safari/iOS 26+, Firefox 156+ 다. Firefox 가 플래그 뒤에 있다는 서술은 낡았다. 다만 MDN 기준 view() 는 아직 not Baseline 이므로, @supports (animation-timeline: scroll()) / @supports (animation-timeline: view()) 로 감싸고 미지원 시 정적 스타일로 fallback 하는 방침은 그대로 유지한다.

  12. view-transition-name: match-element 활용 (Chrome 137+): 수십 개 요소에 수동으로 고유 이름을 부여하는 대신 view-transition-name: match-element CSS 값을 사용하면 브라우저가 자동으로 요소를 매칭한다. 단, 2026-Q2 기준 Chromium 전용이므로 fallback 경로를 유지한다.

  13. 모션은 정적 코드로 입증되지 않는다 — 완료 선언 전 증거 필수 (E2): 이 스킬의 산출물은 재생되어야만 존재를 확인할 수 있다. animate-* 클래스가 붙어 있다는 사실, 코드가 startViewTransition 을 부른다는 사실은 R3(정적) 증거이며 애니메이션이 실제로 재생됐다는 증거가 아니다 — keyframe 미정의, prefers-reduced-motion 상시 적중, 부모의 overflow: hidden 클리핑, 미지원 브라우저 fallback 진입은 전부 조용히 "아무 일도 안 일어남" 으로 끝난다. 완료 직전에 react-kit/references/render-evidence-protocol.md §4 체크리스트를 채운다. 스냅샷 비교로 검증할 때는 전/후 두 시점을 지목한다 — 정지 프레임 1 장은 어떤 모션에도 같은 결과를 내므로 oracle 이 아니다 (§3 d). 증거를 못 얻으면 [미검증] 을 달고 네 칸(막는 것 · 시도한 우회 · 통제 불가 사유 · 재검증 명령 — 규약 §2)을 채워 부분 완료로 보고한다.

    증거 확보를 위해 애니메이션 라이브러리를 도입하지 않는다. Library Policy (Gotcha #1) 는 이 규약보다 상위이며 어떤 검증 편의로도 완화되지 않는다.

  14. 기준 캡처는 편집 전에 찍는다 — react-kit/references/render-evidence-protocol.md §1 Step 0 과 §2 비교 반복 순서의 1 번을 첫 편집 전에 실행하고 그 결과(되말하기 · 관례 표 · 지금 모습의 캡처 경로와 바뀌어야 할 표식)를 응답에 남긴다. 편집한 뒤에는 편집 전 모습을 다시 찍을 수 없다.

  15. Tier 2 는 업데이트 종류로 가른다 — React 19.3 <ViewTransition> 은 안정 API 다 (2026-09-26 추가): react 에서 가져오는 <ViewTransition> 은 19.3 에서 실험 API 가 아니라 안정 API 가 됐다. 감싼 자식이 Transition 으로 표시된 업데이트(startTransition 안의 갱신 · Suspense reveal · useDeferredValue)로 스타일이 바뀌거나 마운트 · 언마운트될 때 돌고, DOM 에서만 동작한다 (출처: https://react.dev/blog/2026/09/09/react-19-3 「We shared it as an experimental API last year, and in 19.3 it's stable and ready to use.」). 그러니 Transition 으로 표시된 React 상태 갱신에는 <ViewTransition> 을 쓰고, 그 밖의 DOM 갱신(Transition 이 아닌 동기 갱신 · React 밖 DOM 조작)에는 §3.1 withViewTransition 래퍼를 그대로 쓴다. 래퍼를 지우지 않는다. react 패키지 안의 컴포넌트라 라이브러리 0개 원칙(#1)에 걸리지 않는다. 프로젝트 react 판이 19.3 미만이면 래퍼만 쓴다

프로세스

1. 자동 티어 판정

--tier 플래그가 없으면 사용자 요청을 분석해 가장 낮은 티어를 선택한다.

키워드 / 시나리오판정 Tier
"fade in", "slide up", "scale", "pulse", "bounce", "hover 효과", "opacity", "shimmer", "진입 애니메이션"T1
"skeleton 로딩 → 완료 전환", "버튼 hover", "accordion", "모달 open/close", "상태 변화"T1
"스크롤 애니메이션", "scroll-driven", "parallax", "스크롤 진행 바", "스크롤 기반"T1
"페이지 전환", "shared element", "grid to board", "뷰 전환", "라우트 전환", "DOM 구조 변경"T2
"scroll-triggered", "스크롤 교차 시 트리거", "특정 위치에서 애니메이션 시작"T2
"드래그앤드롭", "정렬 리스트", "sortable", "kanban", "드래그", "드롭", "gesture", "화살표 연결선"T3

T2/T3 경계가 애매하면 animation-architect-react 에이전트에 자문을 요청한다.

3-Tier 요약:

Tier도구적용 시나리오난이도
T1Tailwind animate-* + CSS @keyframes + scroll-driven상태 변화, hover, 단순 loop, 스크롤 연동낮음
T2View Transitions API (withViewTransition 래퍼) · React 19.3 <ViewTransition> (Transition 으로 표시된 업데이트만 — Gotcha 15)뷰/라우트 전환, shared element, FLIP중
T3Pointer Events + FSM + requestAnimationFrame드래그앤드롭, 제스처, SVG 연결선높음

2. Tier 1 — Tailwind + CSS 구현

2.1 Tailwind 내장 유틸리티

카테고리유틸예시
Transitiontransition-transform, transition-colors, transition-opacityhover:scale-105 transition-transform duration-200
Durationduration-75~duration-1000duration-300
Easingease-linear, ease-in, ease-out, ease-in-outease-in-out
Animation (내장)animate-spin, animate-ping, animate-pulse, animate-bounce로딩, 알림
Delaydelay-75~delay-1000stagger 효과

2.2 tailwindcss-animate 확장 유틸

tailwindcss-animate 플러그인이 제공하는 enter/exit 애니메이션:

  • animate-in / animate-out
  • fade-in-*, fade-out-*
  • slide-in-from-top/bottom/left/right-*, slide-out-to-*
  • zoom-in-*, zoom-out-*

data-[state=...] 속성 선택자와 조합해 상태 기반 애니메이션을 구현한다:

// 드로어 open/close 예시
<div
  data-state={open ? 'open' : 'closed'}
  className="
    data-[state=open]:animate-in data-[state=closed]:animate-out
    data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
    data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left
    duration-300
    motion-reduce:transition-none motion-reduce:animate-none
  "
>
  드로어 내용
</div>

2.3 CSS @keyframes 직접 선언

Tailwind 유틸로 커버 안 되는 커스텀 애니메이션은 globals.css 의 @theme 블록에 선언한다:

/* src/presentation/styles/globals.css */
@import "tailwindcss";

@theme {
  --animate-wiggle: wiggle 1s ease-in-out infinite;
  --animate-shimmer: shimmer 2s linear infinite;

  @keyframes wiggle {
    0%, 100% { transform: rotate(-3deg); }
    50%      { transform: rotate(3deg); }
  }

  @keyframes shimmer {
    from { background-position: -200% 0; }
    to   { background-position: 200% 0; }
  }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

사용: <div className="animate-wiggle">...</div>

2.4 Tier 1 시나리오 예시

Fade-in 진입:

<div className="animate-in fade-in-0 duration-500 motion-reduce:animate-none">
  페이지 로드 시 페이드 진입
</div>

Hover scale 버튼:

<button className="transition-transform duration-200 hover:scale-105 active:scale-95 motion-reduce:transition-none">
  버튼
</button>

스태거 리스트 진입:

{items.map((item, i) => (
  <div
    key={item.id}
    className="animate-in slide-in-from-bottom-4 fade-in-0 motion-reduce:animate-none"
    style={{ animationDelay: `${i * 50}ms`, animationFillMode: 'backwards' }}
  >
    {item.name}
  </div>
))}

2.5 Scroll-driven Animations (CSS 네이티브)

스크롤 위치에 연동되는 애니메이션을 JS 없이 CSS animation-timeline 으로 구현한다. 2026-08-13 기준 scroll() / scroll-timeline 은 Chrome/Edge 115+, Safari/iOS 26+, Firefox 156+ 에서 지원되고 (Can I Use 전역 85.43%), view() 는 MDN 기준 아직 not Baseline 이다. 두 경우 모두 @supports 로 감싸 미지원 브라우저에서 graceful fallback 한다.

스크롤 진행 바:

/* globals.css */
@keyframes progress-grow {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}
<div
  className="fixed top-0 left-0 h-1 w-full origin-left bg-primary motion-reduce:hidden"
  style={{
    animation: 'progress-grow linear',
    animationTimeline: 'scroll(root block)',
  }}
/>

요소 진입 시 fade-in (view timeline):

@keyframes fade-slide-in {
  from { opacity: 0; transform: translateY(2rem); }
  to   { opacity: 1; transform: translateY(0); }
}

@supports (animation-timeline: view()) {
  .scroll-reveal {
    animation: fade-slide-in linear both;
    animation-timeline: view();
    animation-range: entry 0% entry 100%;
  }
}
<section className="scroll-reveal motion-reduce:opacity-100">
  스크롤 시 등장하는 콘텐츠
</section>

주의: scroll-triggered animations (Chrome 145, 2026) 는 특정 스크롤 오프셋 교차 시 시간 기반 애니메이션을 트리거하는 별개 개념이다. scroll-driven 과 혼동하지 않는다. scroll-triggered 는 Tier 2 후보로 분류한다.

3. Tier 2 — View Transitions API 구현

same-document View Transitions 는 MDN 기준 Baseline 2025, Can I Use 기준 전역 90.2% — Chrome/Edge 111+, Safari 18+, Firefox 144+ (2026-08-13 갱신). SPA 내 DOM 변경 시 자동 FLIP 애니메이션.

반면 @view-transition 기반 cross-document / MPA 전환과 일부 세부 기능은 아직 limited availability 다. 라우트 전환에 쓸 때는 same-document 경로를 기본으로 두고 feature detection + fallback 을 유지한다 (§6 표준 커버리지 공백 8 번).

3.1 withViewTransition 래퍼 생성

파일이 없으면 생성한다:

// src/presentation/shared/lib/view-transition.ts
export function withViewTransition(
  updateDOM: () => void,
  options?: { types?: string[] },
): void {
  // prefers-reduced-motion 존중
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
    updateDOM()
    return
  }
  // Feature detection (구형 브라우저 fallback)
  if (typeof document.startViewTransition !== 'function') {
    updateDOM()
    return
  }
  // types 파라미터 지원 여부 분기 (Firefox 초기 구현 미지원)
  try {
    if (options?.types && options.types.length > 0) {
      document.startViewTransition({ update: updateDOM, types: options.types })
    } else {
      document.startViewTransition(updateDOM)
    }
  } catch {
    updateDOM()
  }
}

3.2 view-transition-name CSS 공유 요소

DOM 구조가 바뀌어도 "같은 요소"로 식별해 부드럽게 이동시킨다:

// 동적 id로 고유성 보장 (중복 이름 금지)
<div
  className="rounded-lg border p-4"
  style={{ viewTransitionName: `task-${task.id}` }}
>
  {task.title}
</div>

3.3 그리드 ↔ 보드 뷰 전환 (대표 시나리오)

// src/presentation/features/tasks/components/task-switcher.tsx
import { withViewTransition } from '@/presentation/shared/lib/view-transition'
import { useTaskStore } from '../store'

export function TaskViewSwitcher() {
  const viewMode = useTaskStore((s) => s.viewMode)
  const setViewMode = useTaskStore((s) => s.setViewMode)

  return (
    <button
      onClick={() => {
        withViewTransition(() => {
          setViewMode(viewMode === 'grid' ? 'board' : 'grid')
        })
      }}
    >
      {viewMode === 'grid' ? '보드 뷰로' : '그리드 뷰로'}
    </button>
  )
}

export function TaskList({ tasks }: { tasks: Task[] }) {
  const viewMode = useTaskStore((s) => s.viewMode)
  return (
    <div className={viewMode === 'grid' ? 'grid grid-cols-3 gap-4' : 'flex gap-4 overflow-x-auto'}>
      {tasks.map((task) => (
        <div
          key={task.id}
          className="rounded-lg border p-4"
          style={{ viewTransitionName: `task-${task.id}` }}
        >
          {task.title}
        </div>
      ))}
    </div>
  )
}

3.4 CSS 전환 세부 조정

/* globals.css — 전환 duration + easing */
::view-transition-group(*) {
  animation-duration: 0.4s;
  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

/* 특정 요소 개별 조정 */
::view-transition-group(hero-image) {
  animation-duration: 0.6s;
}

/* reduced-motion 에서 즉시 전환 */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*) {
    animation-duration: 0.01ms;
  }
}

4. Tier 3 — 커스텀 Pointer Primitives 구현

라이브러리 없이 Pointer Events API + 상태 머신(FSM)으로 직접 구현한다.

4.1 useDrag 훅 생성

// src/presentation/shared/hooks/use-drag.ts
import { useCallback, useEffect, useReducer, useRef } from 'react'

// FSM 3 상태: idle / dragging / dropping
type DragState =
  | { kind: 'idle' }
  | { kind: 'dragging'; startX: number; startY: number; offsetX: number; offsetY: number; pointerId: number }
  | { kind: 'dropping'; offsetX: number; offsetY: number }

type DragAction =
  | { type: 'start'; x: number; y: number; pointerId: number }
  | { type: 'move'; x: number; y: number }
  | { type: 'drop' }
  | { type: 'resolved' }
  | { type: 'cancel' }

function dragReducer(state: DragState, action: DragAction): DragState {
  switch (action.type) {
    case 'start':
      return { kind: 'dragging', startX: action.x, startY: action.y, offsetX: 0, offsetY: 0, pointerId: action.pointerId }
    case 'move':
      if (state.kind !== 'dragging') return state
      return { ...state, offsetX: action.x - state.startX, offsetY: action.y - state.startY }
    case 'drop':
      if (state.kind !== 'dragging') return state
      return { kind: 'dropping', offsetX: state.offsetX, offsetY: state.offsetY }
    case 'resolved':
    case 'cancel':
      return { kind: 'idle' }
  }
}

type DragHandlers = {
  onPointerDown: (e: React.PointerEvent) => void
  onPointerMove: (e: React.PointerEvent) => void
  onPointerUp: (e: React.PointerEvent) => void
  onPointerCancel: (e: React.PointerEvent) => void
}

export function useDrag(): {
  state: DragState
  handlers: DragHandlers
  ref: React.RefObject<HTMLElement | null>
  resolve: () => void
} {
  const [state, dispatch] = useReducer(dragReducer, { kind: 'idle' })
  const ref = useRef<HTMLElement | null>(null)

  const onPointerDown = useCallback((e: React.PointerEvent) => {
    const el = ref.current
    if (!el) return
    el.setPointerCapture(e.pointerId)
    dispatch({ type: 'start', x: e.clientX, y: e.clientY, pointerId: e.pointerId })
  }, [])

  const onPointerMove = useCallback((e: React.PointerEvent) => {
    dispatch({ type: 'move', x: e.clientX, y: e.clientY })
  }, [])

  const onPointerUp = useCallback((e: React.PointerEvent) => {
    dispatch({ type: 'drop' })
  }, [])

  const onPointerCancel = useCallback((e: React.PointerEvent) => {
    dispatch({ type: 'cancel' })
  }, [])

  const resolve = useCallback(() => {
    dispatch({ type: 'resolved' })
  }, [])

  return {
    state,
    handlers: { onPointerDown, onPointerMove, onPointerUp, onPointerCancel },
    ref,
    resolve,
  }
}

4.2 컴포넌트 예시

export function DraggableCard({ id, children }: { id: string; children: React.ReactNode }) {
  const { state, handlers, ref, resolve } = useDrag()

  return (
    <div
      ref={ref}
      className={`rounded-lg border p-4 cursor-grab touch-none ${state.kind === 'dragging' ? 'opacity-50 scale-105' : ''}`}
      {...handlers}
      style={{
        transform: state.kind === 'dragging' || state.kind === 'dropping'
          ? `translate(${state.offsetX}px, ${state.offsetY}px)`
          : undefined,
        transition: state.kind === 'dropping' ? 'transform 0.2s ease' : undefined,
      }}
      onTransitionEnd={() => {
        if (state.kind === 'dropping') resolve()
      }}
    >
      {children}
    </div>
  )
}

4.3 Zustand drag store 통합

// src/presentation/shared/stores/drag-store.ts
import { create } from 'zustand'

type DragItem = {
  id: string
  offsetX: number
  offsetY: number
} | null

interface DragStore {
  activeDrag: DragItem
  setActiveDrag: (item: DragItem) => void
  clearDrag: () => void
}

export const useDragStore = create<DragStore>((set) => ({
  activeDrag: null,
  setActiveDrag: (item) => set({ activeDrag: item }),
  clearDrag: () => set({ activeDrag: null }),
}))

5. 검증 및 테스트

5.1 정적 검증

  • /react-audit 명령어를 통해 라이브러리 import 유무, prefers-reduced-motion 누락, view-transition-name 중복 등을 검사한다.
  • 모든 Tier에서 생성된 코드가 Gotcha 리스트를 위반하지 않는지 확인한다.

5.2 런타임 검증 (증거 프로토콜)

Gotcha #13 과 #14 에 따라, 완료 전에 애니메이션이 실제로 재생되는지 확인한다:

  1. 기준 캡처: 편집 전 상태의 스냅샷을 찍거나, 현재 UI 동작을 설명한다.
  2. 편집 적용: 애니메이션 코드를 적용한 후 변경된 상태를 캡처한다.
  3. 증거 비교: 전/후 두 시점의 차이를 명시적으로 제시한다.
  4. 검증 불가 시: [미검증] 태그와 함께 사유를 기록한다.

5.3 브라우저 호환성 체크리스트

  • @supports 또는 canIuse API를 사용하여 최신 브라우저 기능(feature)을 감지하고 fallback을 제공한다.
  • prefers-reduced-motion 설정 시 모든 애니메이션이 비활성화되는지 확인한다.
  • Firefox의 types 파라미터 미지원 등 브라우저별 차이를 고려한다.

Individual skills in this repo

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

相关技能