Communitygithub.com

KILN-skills/scroll-animation

GSAP, ScrollTrigger, SplitText, Lenis를 사용한 스크롤 기반 애니메이션 구축 — 등장, 고정 섹션, 가로 스크롤, 패럴랙스, 스크러빙 타임라인, 텍스트 효과 등을 바닐라 JS, React, Next.js, Vue/Nuxt, Astro에서 올바른 설정, 정리, 반응형 대체 처리와 함께 제공합니다.

scroll-animation란 무엇인가요?

이 스킬은 GSAP, ScrollTrigger, SplitText, Lenis를 활용한 고급 스크롤 기반 애니메이션을 제작하기 위한 포괄적인 가이드입니다. 개발자는 등장 효과, 고정 섹션, 가로 스크롤, 패럴랙스, 스크러빙 타임라인, 텍스트 효과 등을 바닐라 JS, React, Next.js, Vue/Nuxt, Astro 등 다양한 프레임워크에서 일관되고 버그 없는 방식으로 구현할 수 있습니다. 이 스킬은 단순한 API 사용법을 넘어, 실제 사이트에서 자주 발생하는 설정 오류, 프레임워크 통합 방법, 성능 최적화, 반응형 및 접근성(예: prefers-reduced-motion) 대응까지 다룹니다. 특히 ScrollTrigger 사용 시 90%의 버그를 방지하는 핵심 규칙들(예: 타임라인에만 scrollTrigger 적용, scrub 사용 시 ease: none 고정, 생성 순서 준수 등)을 제공하며, gsap.matchMedia()를 통한 조건부 애니메이션과 자동 정리를 강조합니다. 이 스킬은 GreenSock의 공식 스킬과 함께 사용할 때 가장 효과적이며, Lenis와 GSAP ScrollSmoother를 혼용하지 말아야 한다는 점과 프레임워크별 생명주기 관리(React의 useGSAP, Vue의 onMounted/onUnmounted, Astro의 뷰 전환 이벤트 등)에 대한 실용적인 조언을 포함합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/KILN-skills/kiln/tree/HEAD/skills/scroll-animation

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

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

문서

스크롤 애니메이션 (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.

관련 스킬