스크롤 애니메이션 (GSAP + ScrollTrigger + Lenis)
GreenSock의 공식 스킬(gsap-core, gsap-scrolltrigger, gsap-react, gsap-plugins)이 설치되어 있다면 API 상세 내용은 해당 스킬을 사용하세요. 이 스킬은 프로젝트 설정, 통합, 그리고 실제 사이트를 망가뜨리는 흔한 실수들을 다룹니다.
1. 설치
npm i gsap lenis
npm i @gsap/react # React / Next.js 전용
모든 GSAP 플러그인(ScrollTrigger, SplitText, Flip, ScrollSmoother, MorphSVG, DrawSVG…)은 공개 gsap 패키지에 포함되어 있으며 상업적 사용이 무료입니다. gsap/<PluginName>에서 임포트하고 gsap.registerPlugin(...)을 한 번 호출하세요.
2. 단일 설정 모듈
클라이언트에서 한 번 임포트되는 lib/motion/setup.(js|ts)(또는 animations/) 파일을 생성하세요:
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { SplitText } from "gsap/SplitText";
import Lenis from "lenis";
import "lenis/dist/lenis.css";
gsap.registerPlugin(ScrollTrigger, SplitText);
export function initSmoothScroll() {
if (matchMedia("(prefers-reduced-motion: reduce)").matches) return null;
const lenis = new Lenis();
lenis.on("scroll", ScrollTrigger.update);
const tick = (time) => lenis.raf(time * 1000); // GSAP 시간은 초 단위, Lenis는 밀리초
gsap.ticker.add(tick);
gsap.ticker.lagSmoothing(0);
return () => { gsap.ticker.remove(tick); lenis.destroy(); };
}
Lenis 또는 GSAP ScrollSmoother 중 하나만 사용하세요. 둘을 함께 사용하지 마세요.
3. 프레임워크 통합
React / Next.js (App Router) — GSAP를 사용하는 파일은 "use client"가 필요합니다. 항상 useGSAP를 사용하세요(자동 정리, Strict Mode에서 중복 실행 없음):
"use client";
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(useGSAP, ScrollTrigger);
export function Features() {
const root = useRef<HTMLElement>(null);
useGSAP(() => {
const mm = gsap.matchMedia();
mm.add("(prefers-reduced-motion: no-preference)", () => {
gsap.from(".feature", {
autoAlpha: 0, y: 32, stagger: 0.08, ease: "power3.out",
scrollTrigger: { trigger: root.current, start: "top 75%" },
});
});
}, { scope: root }); // 선택자는 이 컴포넌트로 범위가 제한됨
return <section ref={root}>…</section>;
}
- 애니메이션을 생성하는 이벤트 핸들러:
useGSAP의contextSafe로 감싸세요. - 부드러운 스크롤 제공자: 루트 레이아웃의 클라이언트 컴포넌트에서
initSmoothScroll()을 effect 내에서 호출하거나(또는lenis/react의ReactLenis를autoRaf: false로 설정하고 GSAP 티커와 함께 사용). - Next.js에서 라우트 변경: 최상단으로 스크롤(
lenis.scrollTo(0, { immediate: true }))하고 새 페이지가 마운트된 후ScrollTrigger.refresh()를 호출하세요.
Vue / Nuxt — onMounted 내부에서 gsap.context(() => {…}, rootEl)를 사용해 생성하고, onUnmounted에서 ctx.revert()를 호출하세요. Nuxt: 클라이언트 전용 플러그인(plugins/gsap.client.ts).
Astro / 정적 멀티페이지 — 레이아웃의 <script> 태그에서 사용. View Transitions 사용 시 astro:page-load에서 재초기화하고 astro:before-swap에서 트리거를 제거하세요.
바닐라 — DOM 준비 후 실행되는 단일 main.js에서 각 섹션을 gsap.context()로 그룹화하세요.
4. 90%의 버그를 방지하는 ScrollTrigger 규칙
scrollTrigger를 타임라인 또는 최상위 트윈에만 설정하세요. 타임라인 내부의 중첩 트윈에는 절대 설정하지 마세요.scrub사용 시ease: "none"을 사용하세요.- 생성 순서가 중요합니다: 페이지 순서대로 트리거를 위에서 아래로 생성하세요(또는
refreshPriority설정). 특히 고정(pin) 효과와 함께 사용할 때 중요합니다. - 애니메이션을 적용할 요소 자체가 아니라 **래퍼(wrapper)**를 고정(pin)하세요.
transform이 적용된 부모 내부에서는 고정하지 마세요. 의도된 경우가 아니라면pinSpacing: false를 피하세요. - 뷰포트 크기에 의존하는 모든 것에 함수 값(
x: () => …) +invalidateOnRefresh: true를 사용하세요. - 이미지/폰트/동적 콘텐츠가 레이아웃을 변경한 후
ScrollTrigger.refresh()를 호출하세요(window.addEventListener("load", …),document.fonts.ready). markers: true로 디버깅하세요. 배포 전에 제거하세요.- 초기 숨김 상태: JS로 설정된
gsap.from/fromTo또는autoAlpha를 사용하세요. 그래야 JS가 없어도 콘텐츠가 보입니다. once: true는 재생되지 않아야 하는 등장 효과(대부분)에 사용하세요.
5. 반응형 및 접근성(동작 감소)
모든 장면을 gsap.matchMedia()로 감싸세요:
const mm = gsap.matchMedia();
mm.add({
isDesktop: "(min-width: 1024px)",
isMobile: "(max-width: 1023px)",
motionOK: "(prefers-reduced-motion: no-preference)",
}, ({ conditions: c }) => {
if (!c.motionOK) return; // 정적 레이아웃, 애니메이션 없음
if (c.isDesktop) { /* 고정 / 가로 스크롤 장면 */ }
if (c.isMobile) { /* 간단한 페이드 업 효과만 */ }
});
matchMedia는 조건이 변경될 때 자동으로 되돌립니다(revert). 수동 정리 불필요.
6. 성능
x/y/scale/rotation/autoAlpha(변환 + 불투명도)를 애니메이션하세요.width/height/top/left및 넓은 영역에 대한 무거운filter: blur()애니메이션은 피하세요.- 수십 개의 개별 트리거보다 섹션당 하나의 타임라인을 선호하세요. 동일한 등장 효과가 많으면
ScrollTrigger.batch를 사용하세요. - 포인터 추적 효과에는
gsap.quickTo를 사용하세요. - 스크러빙 장면 내의 큰 이미지: 적절한
sizes속성을 사용하고, 매 프레임마다 거대한 비트맵을 크기 조정하지 마세요.
7. 확인
전체 페이지를 빠르게 위아래로 스크롤하고, 창 크기를 조정하고, 페이지 중간에서 새로고침하고, 다른 페이지로 이동했다가 돌아오세요(SPA) — 점프, 중복 핀, 콘솔 오류가 없어야 합니다. 동작 감소 모드 켜짐, 375px 너비, CPU 스로틀링 상태에서 테스트하세요.
일반적인 효과에 대한 레시피: ../motion-direction/references/patterns.md.