비디오 및 프로토타입 모션 추출 (video-to-gsap)은(는) 무엇을 하나요?
1. 목적 및 트리거
유연한 미세 상호작용과 영화 같은 모션은 고급 상거래 경험을 차별화합니다. 그러나 잘못 구현된 웹 애니메이션은 스레드 차단, 프레임 드롭(잔크), 메모리 누수, 그리고 낮은 INP(Interaction to Next Paint)를 유발합니다. 이 스킬은 부드러운 60/120 FPS 애니메이션의 추출 및 구현을 관리합니다.
트리거:
- 사용자가 비디오 클립, GIF 또는 Figma 모션 프로토타입을 제공할 때
- 카트 슬라이드오버, 페이지 전환, 아코디언 확장, 제품 카드 호버, 또는 체크아웃 완료 축하 애니메이션을 만들 때
- ScrollTrigger를 사용한 스크롤 기반 애니메이션을 만들 때
2. 모션 분해 프로토콜
모션 클립을 분석할 때 체계적으로 다음을 식별합니다:
- 코레오그래피 시퀀스:
- 스테이징 / 트리거: 어떤 동작이 모션을 시작하는가 (호버, 스크롤, 경로 변경, 클릭)?
- 준비 동작: 주 모션 전에 미묘한 역방향 움직임이 있는가?
- 동작 및 이동: 어떤 요소가 어떤 방향으로, 얼마나 멀리 (px, rem 또는 %) 이동하는가?
- 정착 및 후속 동작: 오버슈팅(스프링/탄성)하는가 아니면 부드럽게 감속하는가 (
power3.out,expo.out)?
- 타이밍 분석:
- 각 요소의 지속 시간 (예: 서랍 진입:
450ms, 배경 페이드:300ms). - 스태거된 자식들 사이의 stagger 오프셋 (예: 제품 그리드 카드:
0.06sstagger).
- 각 요소의 지속 시간 (예: 서랍 진입:
- Easing 시그니처:
- 선형: 연속 루프에만 사용 (예: 마키).
- Ease-out (
power2.out,expo.out): UI 진입용 (빠른 진입, 부드러운 정착). - Ease-in (
power2.in): UI 종료용 (화면 밖으로 가속). - Ease-in-out (
power3.inOut): 뷰 내 상태 변환용.
3. 성능 및 하드웨어 가속 표준
규칙 1: 합성 속성만 애니메이션
- 허용됨:
transform(x,y,xPercent,yPercent,scale,rotation,skew) 및opacity(또는 GSAPautoAlpha). - 엄격히 금지: 레이아웃 또는 페인트 속성 (
width,height,top,left,right,bottom,margin,padding,border-width)을 애니메이션하는 것. 이는 브라우저 CPU가 매 프레임마다 레이아웃 지오메트리를 다시 계산하고 픽셀을 다시 그리도록 강제하여 INP 저하 및 프레임 드롭을 유발합니다.
규칙 2: Scoped useGSAP으로 메모리 누수 제로
- 항상 공식
@gsap/react패키지와useGSAP훅을 사용하세요. scope컨테이너 ref를 제공하세요. 내부의 모든 선택자는 자동으로 스코프 지정되며, 모든 타임라인, 트윈 및 ScrollTrigger 인스턴스는 컴포넌트가 언마운트될 때 깔끔하게 복원되고 제거됩니다.- 이는 싱글 페이지 앱 및 React 19 빠른 새로고침 사이클에서 좀비 이벤트 리스너와 메모리 보유를 방지합니다.
규칙 3: FOUC(스타일 적용 전 깜빡임) 제거
opacity대신autoAlpha를 사용하세요.autoAlpha는 불투명도가 0에 도달하면visibility: hidden을 설정하여 하이드레이션 전에 고스트 클릭과 렌더링 깜빡임을 방지합니다.- 또는 CSS 기본 숨김 상태를 선언하고
gsap.fromTo로 표시하세요.
4. 프로덕션 구현 청사진
"use client";
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
import { ScrollTrigger } from "gsap/ScrollTrigger";
// Ensure plugins are registered safely
if (typeof window !== "undefined") {
gsap.registerPlugin(useGSAP, ScrollTrigger);
}
interface ProductShowcaseProps {
readonly items: Array<{ id: string; title: string; price: string }>;
}
export function ProductShowcase({ items }: ProductShowcaseProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
useGSAP(
() => {
// Hardware-accelerated entrance timeline
const tl = gsap.timeline({
defaults: {
ease: "expo.out",
duration: 0.8,
},
});
tl.from(".showcase-header", {
autoAlpha: 0,
y: 30,
clearProps: "all", // Clean up inline styles post-animation
}).from(
".product-card",
{
autoAlpha: 0,
y: 40,
scale: 0.96,
stagger: 0.08,
duration: 0.7,
clearProps: "transform,opacity",
},
"-=0.4" // Seamless overlap
);
},
{ scope: containerRef } // Scoped cleanup boundary
);
return (
<section ref={containerRef} className="w-full py-16 px-6">
<div className="showcase-header max-w-4xl mx-auto text-center mb-12">
<h2 className="text-h1 font-bold text-content-primary">Featured Collection</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-7xl mx-auto">
{items.map((item) => (
<article
key={item.id}
className="product-card will-change-transform bg-surface-elevated rounded-lg p-6 border border-border-subtle"
>
<h3 className="text-h3 font-semibold text-content-primary">{item.title}</h3>
<p className="text-content-secondary mt-2">{item.price}</p>
</article>
))}
</div>
</section>
);
}
5. 축소된 모션 준수
항상 축소된 모션을 선호하는 사용자를 존중하세요:
const { contextSafe } = useGSAP({ scope: containerRef });
// Check accessibility preference
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (prefersReducedMotion) {
// Instantly render in final resting state without animation
gsap.set([".showcase-header", ".product-card"], { autoAlpha: 1, y: 0, scale: 1 });
return;
}