프론트엔드 동적 효과 (Frontend Animation)
프론트엔드 동적 효과(애니메이션)를 설계, 구현, 검토, 최적화합니다. 기본적으로 React + TypeScript 프로젝트를 가정합니다. 순수 JS나 다른 프레임워크는 프로젝트 현황에 따라 처리하며, Vue나 네이티브 프로젝트에 React 방식을 강요하지 않습니다.
동적 효과는 향상이지 장식이 아닙니다. 정적 버전이 먼저 보기 좋고 사용 가능해야 하며, 그 후에 동적 효과를 추가합니다.
사용자의 확인 없이 새로운 종속성을 추가해서는 안 됩니다.
§0 작업 전 필수: 종속성 및 디자인 시스템 탐색
작업을 시작하기 전에 다음 다섯 단계를 순서대로 완료하십시오. 한 단계라도 누락되면 먼저 탐색을 수행한 후 코드를 작성하십시오.
package.json을 읽고,pnpm-lock.yaml/package-lock.json/yarn.lock에서 실제로 잠긴 버전을 확인하여 설치된 애니메이션 라이브러리와 메이저 버전을 확인합니다.tailwind.config.*, 전역 스타일(globals.css/index.css/app.css), 테마 파일을 읽고 기존 토큰, 지속 시간, 이징 및 명명 규칙을 찾습니다.- motion이 있으면 GSAP를 사용하지 말고, GSAP가 있으면 motion을 추가하지 마십시오. 동일한 요구 사항에 대해 두 라이브러리를 동시에 사용할 수 없습니다.
- 함부로 새 종속성을 추가하지 마십시오. 꼭 필요한 경우, 패키지 이름, min+gzip 크기, 대안, 기존 라이브러리로는 부족한 이유를 설명하고 사용자 확인을 받은 후 설치하십시오.
- 프로젝트의 기존 명명 규칙, 파일 구조, 코드 스타일 및 토큰 이름을 따르고, 두 번째 동적 효과 시스템을 도입하지 마십시오.
| 탐색 결과 | 기본 동작 |
|---|---|
| 이미 motion 있음 | 인터랙션, 진입/퇴장, 레이아웃 애니메이션 모두 motion 사용, GSAP 도입 금지 |
| 이미 framer-motion (구버전) | 기존 패키지명과 API 사용, motion으로 마이그레이션 금지; 업그레이드는 별도로 제안하고 별도로 수행 |
| 이미 GSAP 있음 | 타임라인 및 스크롤 연동은 GSAP + ScrollTrigger 사용; 간단한 hover는 CSS transition 유지, motion 추가 금지 |
| Tailwind만 있음 | transition / duration / ease 원자 클래스와 전역 CSS 변수 사용, 라이브러리 설치 금지 |
| 순수 CSS 프로젝트 | CSS transition/animation + WAAPI만 사용, JS 애니메이션 라이브러리 설치 금지 |
| Next.js App Router | 애니메이션 컴포넌트에 "use client" 표시; View Transitions 및 motion은 클라이언트 컴포넌트로 처리, 라우트 간 공유 상태 해킹 금지 |
| Vue | 네이티브 CSS 또는 Vue 생태계의 motion 방식 사용, React 전용 라이브러리 강요 금지 |
§1 안티 패턴 목록 (위반 불가)
항목별로 대조하십시오. 하나라도 해당되면 다시 작성해야 하며, "이번에는 넘어가자"는 타협을 하지 않습니다.
| 금지 | 대안 |
|---|---|
transition: all .3s | transform, opacity를 명시적으로 나열하고, 필요시 color 추가 |
전역 통일 ease-in-out | 진입/퇴장/양방향 세 가지 맥락에 따라 곡선 선택 (§3 참조) |
요소가 opacity: 0 + scale(.8)로 튀어나옴 | opacity: .9→1 + translateY(8px)→0 |
무분별한 linear | 진행률 표시줄과 무한 루프 장식에만 사용 가능 |
| 동시에 3개 이상의 요소가 애니메이션 중 | 하나의 시각적 초점만 유지, 나머지는 정지 또는 매우 미약하게 |
hover 시 transform: translateY(-4px)를 사용하지만 부모 컨테이너에 공간 확보 없음 | scale을 사용하거나 부모에 padding을 추가하여 공간 확보 |
| 진입 애니메이션만 있고 퇴장 애니메이션 없음 | 반드시 쌍으로 설계하고, 퇴장이 진입보다 빠르게 |
넓은 영역에 filter: blur() / drop-shadow를 애니메이션 | 사전 렌더링된 블러 레이어를 사용하거나 애니메이션 범위 축소 |
| 애니메이션 레이어가 CTA / 폼을 가림 | 동적 효과 레이어에 pointer-events: none 설정, 또는 인터랙션 영역 회피 |
| 페이지 첫 로딩 애니메이션이 1.5초 초과 | 초과 시 먼저 스켈레톤 스크린 제공 |
| 사용자 정의 커서 + 잔상 효과 (사용자가 명시적으로 요청하지 않은 경우) | 하지 않음 |
| 모든 목록 항목에 복잡한 글자/프레임별 애니메이션 추가 | 30–50ms stagger를 사용한 전체 이동만으로 충분 |
setTimeout으로 애니메이션 연결 | transition/animation 이벤트, onAnimationComplete, timeline 사용 |
프레임별 el.style.left = x | WAAPI / rAF / 라이브러리의 timeline 사용 |
임계값 이유: 첫 화면을 1.5초 이상 기다리면 사용자가 페이지가 멈췄다고 의심하기 시작하므로 먼저 스켈레톤 스크린을 제공합니다. 동시에 3개 이상의 독립적인 움직임이 있으면 시각적 초점이 분산되어 사용자가 어디를 봐야 할지 모릅니다.
§2 선택 결정 트리 (요약본)
먼저 표를 보고 방안을 결정한 후, §3으로 이동하여 수치를 정하고, 그 다음에야 코드를 작성합니다.
| 시나리오 | 첫 번째 선택 | 사용 금지 |
|---|---|---|
| hover / press / toggle 피드백 | CSS transition | GSAP (과도함) |
| 컴포넌트 진입/퇴장, 목록 추가/삭제, 레이아웃 변경 | motion (AnimatePresence + layout) | 수동 height 애니메이션 |
| 라우트/페이지 전환 | View Transitions API, 또는 motion layoutId | 전체 화면 오버레이 + 지연 |
| 복잡한 타임라인, 시퀀스 편성, 스크롤 연동 | GSAP + ScrollTrigger | CSS keyframes 남용 |
| 3D / 입자 / 물리 / 셰이더 | three.js + React Three Fiber + drei | CSS로 3D 흉내내기 |
| 디자이너가 제공한 벡터 애니메이션 | dotLottie / Lottie | GIF로 변환 |
| 간단한 아이콘 또는 패스 애니메이션 | SVG + CSS | Lottie 런타임 도입 |
| 데이터 차트 동적 효과 | 라이브러리 자체 애니메이션 (예: Recharts/visx의 transition) | 직접 패스 tween |
| 배경 장식 (그리드/웨이브/광채) | CSS 또는 canvas + requestAnimationFrame, 일시 중지 가능 | three.js (진정한 3D가 필요한 경우가 아니라면) |
| 숫자/금액 변화 | tabular-nums + 간단한 tween | 프레임별 레이아웃 재구성 |
전체 선택 로직, 질문 순서 및 "CSS → 경량 라이브러리 → 중량 라이브러리" 업그레이드 단계는 references/decision-tree.md를 참조하십시오.
§3 수치 규범 (감으로 결정 금지)
모든 지속 시간, 곡선, 폭은 아래 표에서 값을 가져옵니다. 표에 없는 값은 먼저 논증한 후에 새로 추가해야 합니다.
지속 시간
| 항목 | 값 |
|---|---|
| hover / press | 120–180ms |
| toggle / 펼치기/접기 | 200–300ms |
| 진입 | 240–400ms |
| 퇴장 | 진입 시간 × 0.6–0.7 |
| 페이지 전환 | 300–500ms |
| 레이아웃 재구성 (layout 애니메이션) | 300–500ms |
| stagger 간격 | 30–50ms, 전체 그룹 총 시간 ≤ 500ms |
| 600ms 초과 | 이유를 설명할 수 있어야 하며, 그렇지 않으면 제거 |
이징 (Easing)
| 이징 | 곡선 |
|---|---|
| 진입 | cubic-bezier(0.16, 1, 0.3, 1) |
| 퇴장 | cubic-bezier(0.4, 0, 1, 1) |
| 양방향/상태 전환 | cubic-bezier(0.4, 0, 0.2, 1) |
| 이동/드래그/제스처 | spring: stiffness 300–500, damping 30–40 |
| linear | 진행률 표시줄, 무한 루프 장식에만 사용 |
폭 (Amplitude)
| 폭 | 값 |
|---|---|
| 이동 | 미세 4–8px / 중간 12–24px / 큼 32–64px / >80px는 전체 화면 전환에만 사용 |
| 투명도 | 미세 인터랙션 .9→1; 진입 .6→1; 0→1은 전체 화면 오버레이와 모달에만 사용 |
| 스케일 | 0.96–1.04 범위만 |
Spring
| 매개변수 | 값 | 이유 |
|---|---|---|
| stiffness | 300–500 | 300 미만은 둔하고, 500 초과는 흔들려서 고장처럼 보임 |
| damping | 30–40 | 30 미만은 튕김이 과하고, 40 초과는 튕김 느낌이 없음 |
| mass | 0.8–1 | 관성을 느낄 수 있는 최소한의 변경 |
구간 이유, 한 줄 설명:
- hover / press 120–180ms: 120ms 미만은 상태 변화를 읽을 수 없고, 180ms 초과는 피드백이 지연되어 사용자가 빈 클릭을 했다고 생각합니다.
- 진입 240–400ms: 240ms 미만은 이동 방향을 읽을 수 없고, 400ms 초과는 사용자가 기다리기 시작합니다.
- 퇴장 × 0.65: 떠나는 것은 선명하게 보일 필요가 없으며, 느리면 지루합니다.
- stagger 30–50ms: 30ms 미만은 동시에 나타나는 것처럼 보이고, 50ms 초과는 전체 그룹이 500ms 인내심 한계를 넘습니다.
이러한 값은 CSS 변수와 TS 상수로 이미 정리되어 있습니다 (references/tokens.md 참조). 모든 코드는 토큰을 참조해야 하며, 숫자를 하드코딩하는 것을 금지합니다.
§4 성능 및 접근성 레드라인
transform/opacity만 애니메이션합니다. 크기를 애니메이션해야 한다면 FLIP 또는 라이브러리의layout기능을 사용합니다.- 지속적으로 애니메이션되는 레이어는
will-change: transform을 추가하고, 애니메이션 종료 후 제거합니다. - 긴 목록은 가상화합니다. 화면 밖 요소는 IntersectionObserver를 사용하여 일시 중지합니다.
prefers-reduced-motion: reduce는 필수 분기이며, 선택적 보너스 항목이 아닙니다. 다운그레이드 후에도 명확한 상태 변화가 있어야 합니다. 일반적으로 ≤120ms의 opacity 교차 페이드(120ms는 상태 전환을 읽기에 충분하며, 이해해야 할 애니메이션으로 간주되지 않음)로 축소하거나 직접 전환합니다.- 초당 3회 이상의 깜빡임을 사용하지 마십시오 (WCAG 2.3.1의 광과민성 임계값이 초당 3회입니다).
- 애니메이션 중에 포커스를 빼앗거나 탭 순서를 방해하지 마십시오. 키보드 포커스 링은 애니메이션에 참여하지 않습니다.
- "정적 버전도 보기 좋게" 디자인하고, 동적 효과는 향상일 뿐입니다.
WCAG 조항, aria-live, 테스트 방법은 references/accessibility.md를 참조하십시오.
§5 구현 철칙
- 진입과 퇴장을 쌍으로 설계하고, 퇴장이 더 빠릅니다.
- 한 화면에 하나의 시각적 초점만 유지합니다.
- stagger는 30–50ms를 사용하고, 전체 그룹은 500ms를 초과하지 않습니다.
- 장식 동적 효과 레이어는 반드시
pointer-events: none이어야 합니다. - transition/animation 이벤트,
onAnimationComplete또는 timeline을 사용하여 연결하고,setTimeout은 금지합니다. - CSS 또는 Framer로 처리할 수 있으면 GSAP를 사용하지 않습니다.
transform을 사용할 수 있으면width/height를 애니메이션하지 않습니다.- 애니메이션 지속 시간은 디자인 리뷰에 포함되며, "느낌상 조금 느리게"라는 구두 조정에 의존하지 않습니다.
- 먼저 줄이고, 그 다음에 추가합니다. 삭제할 수 있는 동적 효과는 먼저 삭제하고, 삭제할 수 없는 것만 최적화합니다.
§6 검토 모드
기존 동적 효과를 검토할 때, 각 항목은 고정된 네 부분으로 출력됩니다:
문제 / 근거 (§1–§4 조항 참조) / 제안 코드 / 영향 (지속 시간, 성능, 접근성)
규칙:
- 실제 문제만 보고합니다. 숫자를 채우기 위해 합리적인 구현을 수정하지 않습니다.
- 최대 8개 항목이며, 심각도 순으로 정렬합니다: 접근성 > 성능 > 지속 시간/곡선 > 스타일.
- 끝에 "이 부분은 올바릅니다"라는 문단을 작성하여 1–3개 항목을 나열하고, 사용자가 합리적인 구현을 잘못 수정하지 않도록 합니다.
스캔 명령어 (읽기 전용이며, --fix를 제공하지 않습니다. 자동으로 애니메이션을 수정하면 디자인 의도가 망가집니다):
node ~/.claude/skills/frontend-animation/scripts/audit-animation.mjs <프로젝트 디렉토리> --json
스크립트는 문제와 위치만 보고하며, 수정은 사람이 결정합니다.
§7 X가 필요할 때 무엇을 읽을 것인가
| 증상/요구사항 | 파일 |
|---|---|
| CSS를 사용할지 라이브러리를 사용할지 모르겠거나, 프레임워크 간 선택을 해야 하거나, 순서대로 질문해야 할 때 | references/decision-tree.md |
| 토큰을 적용하거나, Tailwind에 연결하거나, CSS 변수 또는 TS 상수를 작성해야 할 때 | references/tokens.md |
| motion / GSAP / three.js+R3F / Lottie / View Transitions의 설치, API, 버전 문제를 확인해야 할 때 | references/libraries.md |
| 바로 사용 가능한 템플릿이 필요할 때 (목록 진입 / 라우트 전환 / 드래그 / 마우스 따라다니기 / 숫자 롤링 / 3D 뒤집기 / 루프 배경 / 스켈레톤 스크린 / 자기 버튼 / Lottie) | references/patterns.md |
| 이징 곡선을 선택하거나, spring의 linear() 근사값을 찾거나, 특정 곡선의 이유를 설명해야 할 때 | references/easing.md |
| reduced-motion 다운그레이드, WCAG, 깜빡임 및 포커스, 테스트 방법 | references/accessibility.md |
| 완전한 reduced-motion 분기가 포함된 참조 컴포넌트가 필요할 때 | assets/template-component.tsx |
| 기존 코드에서 애니메이션 문제를 스캔해야 할 때 | scripts/audit-animation.mjs |
§8 커밋 전 자가 점검 (8문항, 항목별 답변)
- 이 동적 효과가 필요한가? 제거해도 정적 버전이 여전히 보기 좋은가?
transform/opacity만 애니메이션했는가?- 지속 시간, 곡선, 폭이 모두 §3 범위 내에 있는가?
- 진입과 퇴장이 쌍으로 되어 있고, 퇴장이 더 빠른가?
- reduced-motion에 분기가 있는가? 다운그레이드 후에도 상태를 구별할 수 있는가?
- 새 종속성을 추가했는가? 확인 없이 추가했다면 위반이다.
- 초당 3회 이상 깜빡임이 있는가? 애니메이션 중에 포커스를 빼앗는가?
- 한 화면에 시각적 초점이 하나만 있는가?
하나라도 답변할 수 없으면 멈추고 수정한 후 커밋하십시오.