Vue, Svelte 및 기타 프레임워크에서의 GSAP
이 스킬을 사용하는 경우
Vue (또는 Nuxt), Svelte (또는 SvelteKit), 또는 생명주기(마운트/언마운트)를 사용하는 기타 컴포넌트 프레임워크에서 GSAP 코드를 작성하거나 검토할 때 적용합니다. React의 경우에는 gsap-react 스킬(useGSAP 훅, gsap.context())을 사용하세요.
관련 스킬: 트윈과 타임라인에는 gsap-core 및 gsap-timeline; 스크롤 기반 애니메이션에는 gsap-scrolltrigger; React에는 gsap-react를 사용하세요.
원칙 (모든 프레임워크)
- 트윈과 ScrollTrigger는 컴포넌트의 DOM을 사용할 수 있게 된 후 (예: onMounted, onMount)에 생성하세요.
- 언마운트(또는 이에 상응하는) 정리 단계에서 **제거(kill) 또는 되돌리기(revert)**를 수행하여 분리된 노드에서 실행되거나 메모리 누수가 발생하지 않도록 하세요.
- 선택자를 컴포넌트 루트로 스코프하여
.box등의 선택자가 페이지의 다른 요소가 아닌 해당 컴포넌트 내 요소만 일치하도록 하세요.
Vue 3 (Composition API)
실행 가능한 Vite + Vue 3 프로젝트 예제는 examples/vue/ 디렉토리를 참조하세요.
onMounted를 사용하여 컴포넌트가 DOM에 삽입된 후 GSAP를 실행하세요. onUnmounted를 사용하여 정리합니다.
import { onMounted, onUnmounted, ref } from "vue";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger); // 앱 당 한 번 (예: main.js)
export default {
setup() {
const container = ref(null);
let ctx;
onMounted(() => {
if (!container.value) return;
ctx = gsap.context(() => {
gsap.to(".box", { x: 100, duration: 0.6 });
gsap.from(".item", { autoAlpha: 0, y: 20, stagger: 0.1 });
}, container.value);
});
onUnmounted(() => {
ctx?.revert();
});
return { container };
},
};
- ✅ gsap.context(scope) — 컨테이너 ref(예:
container.value)를 두 번째 인자로 전달하여.item과 같은 선택자가 해당 루트로 스코프되도록 합니다. 콜백 내에서 생성된 모든 애니메이션과 ScrollTrigger는 추적되며 ctx.revert() 가 호출될 때 되돌려집니다. - ✅ onUnmounted — 항상 ctx.revert() 를 호출하여 트윈과 ScrollTrigger가 제거되고 인라인 스타일이 원래대로 복원되도록 합니다.
Vue 3 (script setup)
<script setup>과 ref를 사용한 동일한 패턴:
<script setup>
import { onMounted, onUnmounted, ref } from "vue";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
const container = ref(null);
let ctx;
onMounted(() => {
if (!container.value) return;
ctx = gsap.context(() => {
gsap.to(".box", { x: 100 });
gsap.from(".item", { autoAlpha: 0, stagger: 0.1 });
}, container.value);
});
onUnmounted(() => {
ctx?.revert();
});
</script>
<template>
<div ref="container">
<div class="box">Box</div>
<div class="item">Item</div>
</div>
</template>
Nuxt 4
플러그인 등록, 지연 로딩 및 SSR-안전 패턴을 포함한 실행 가능한 Nuxt 4 프로젝트는
examples/nuxt/를 참조하세요.
재사용 가능한 컴포저블(composable) 을 사용하여 GSAP 플러그인을 등록하고, 애플리케이션에서 많이 사용되지 않는 플러그인을 지연 로딩합니다:
// composables/useGSAP.ts
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
const PLUGINS = [
"CSSRulePlugin",
"CustomBounce",
"CustomEase",
"CustomWiggle",
"Draggable",
"DrawSVGPlugin",
"EaselPlugin",
"EasePack",
"Flip",
"GSDevTools",
"InertiaPlugin",
"MorphSVGPlugin",
"MotionPathHelper",
"MotionPathPlugin",
"Observer",
"Physics2DPlugin",
"PhysicsPropsPlugin",
"PixiPlugin",
"ScrambleTextPlugin",
"ScrollSmoother",
"ScrollToPlugin",
"ScrollTrigger",
"SplitText",
"TextPlugin",
] as const;
type Plugins = (typeof PLUGINS)[number];
// 모든 GSAP 플러그인을 동적으로 로드하기 위한 매핑
const pluginMap = {
CustomEase: () => import("gsap/CustomEase"),
Draggable: () => import("gsap/Draggable"),
CSSRulePlugin: () => import("gsap/CSSRulePlugin"),
EaselPlugin: () => import("gsap/EaselPlugin"),
EasePack: () => import("gsap/EasePack"),
Flip: () => import("gsap/Flip"),
MotionPathPlugin: () => import("gsap/MotionPathPlugin"),
Observer: () => import("gsap/Observer"),
PixiPlugin: () => import("gsap/PixiPlugin"),
ScrollToPlugin: () => import("gsap/ScrollToPlugin"),
ScrollTrigger: () => import("gsap/ScrollTrigger"),
TextPlugin: () => import("gsap/TextPlugin"),
DrawSVGPlugin: () => import("gsap/DrawSVGPlugin"),
Physics2DPlugin: () => import("gsap/Physics2DPlugin"),
PhysicsPropsPlugin: () => import("gsap/PhysicsPropsPlugin"),
ScrambleTextPlugin: () => import("gsap/ScrambleTextPlugin"),
CustomBounce: () => import("gsap/CustomBounce"),
CustomWiggle: () => import("gsap/CustomWiggle"),
GSDevTools: () => import("gsap/GSDevTools"),
InertiaPlugin: () => import("gsap/InertiaPlugin"),
MorphSVGPlugin: () => import("gsap/MorphSVGPlugin"),
MotionPathHelper: () => import("gsap/MotionPathHelper"),
ScrollSmoother: () => import("gsap/ScrollSmoother"),
SplitText: () => import("gsap/SplitText"),
} as const;
type PluginMap = typeof pluginMap;
type Plugins = keyof PluginMap;
// 주어진 키에 대한 모듈 타입을 확인하고, 키와 일치하는 named export를 추출합니다.
// 이를 통해 코드 편집기에서 자동 완성을 위한 타입 정의를 사용할 수 있습니다.
type PluginModule<K extends Plugins> = Awaited<ReturnType<PluginMap[K]>>;
type PluginExport<K extends Plugins> = PluginModule<K>[K & keyof PluginModule<K>];
export default function () {
// 이 시점에서 원하는 모든 GSAP 플러그인을 등록합니다.
gsap.registerPlugin(ScrollTrigger);
/*
앱에서 널리 사용되지 않는 플러그인(예: 몇몇 컴포넌트나 단일 라우트에서만 사용)을
지연 로딩하려면 이 메서드를 사용할 수 있습니다.
*/
async function lazyLoadPlugin<K extends Plugins>(plugin: K): Promise<PluginExport<K>> {
const loader = pluginMap[plugin];
const m = await loader();
const p = (m as any)[plugin];
gsap.registerPlugin(p);
return p;
}
return {
gsap,
ScrollTrigger,
lazyLoadPlugin,
};
}
useGSAP()를 통해 컴포넌트에서 접근:
const { gsap, ScrollTrigger, lazyLoadPlugin } = useGSAP();
- ✅
useGSAP()는 타입이 지정된 gsap 인스턴스와 지연 로딩 메서드를 제공합니다. - ✅ 모든 플러그인을 지연 로딩하여 (SplitText, MorphSVG 등) 앱에서 널리 사용되지 않는 경우 초기 번들 크기를 줄입니다.
- ✅ Vue 3와 동일하게 컴포넌트에서 gsap.context(scope) 및 onUnmounted → ctx.revert() 를 사용합니다.
Svelte
onMount를 사용하여 DOM이 준비된 후 GSAP를 실행합니다. onMount의 반환 cleanup 함수를 사용하거나 (컨텍스트를 추적하여 반응형 블록/컴포넌트 소멸 시 정리)하여 되돌리기(revert)를 수행합니다. Svelte 5는 다른 생명주기를 사용하지만 동일한 원칙이 적용됩니다: "마운트" 시점에 생성하고 "소멸" 시점에 되돌립니다.
<script>
import { onMount } from "svelte";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
let container;
onMount(() => {
if (!container) return;
const ctx = gsap.context(() => {
gsap.to(".box", { x: 100 });
gsap.from(".item", { autoAlpha: 0, stagger: 0.1 });
}, container);
return () => ctx.revert();
});
</script>
<div bind:this={container}>
<div class="box">Box</div>
<div class="item">Item</div>
</div>
- ✅ bind:this={container} — 루트 요소에 대한 참조를 얻어 **gsap.context(scope)**에 전달합니다.
- ✅ return () => ctx.revert() — Svelte의 onMount는 cleanup 함수를 반환할 수 있습니다; 여기서 ctx.revert() 를 호출하여 컴포넌트가 소멸될 때 정리되도록 합니다.
선택자 스코프 설정
현재 컴포넌트 외부의 요소를 일치시킬 수 있는 전역 선택자를 사용하지 마세요. 항상 scope (컨테이너 요소 또는 ref)를 gsap.context(callback, scope) 의 두 번째 인자로 전달하여 콜백 내에서 실행되는 모든 선택자가 해당 하위 트리로 제한되도록 하세요.
- ✅ gsap.context(() => { gsap.to(".box", ...) }, containerRef) —
.box는containerRef내에서만 검색됩니다. - ❌ 컴포넌트에서 컨텍스트 스코프 없이 gsap.to(".box", ...) 를 실행하면 다른 인스턴스나 페이지의 나머지 부분에 영향을 줄 수 있습니다.
ScrollTrigger 정리
ScrollTrigger 인스턴스는 트윈/타임라인에 scrollTrigger 설정을 사용하거나 **ScrollTrigger.create()**를 사용할 때 생성됩니다. 이들은 **gsap.context()**에 포함되며 **ctx.revert()**를 호출하면 되돌려집니다. 따라서:
- 트윈에 사용하는 동일한 gsap.context() 콜백 내에서 ScrollTrigger를 생성하세요.
- 트리거 위치에 영향을 주는 레이아웃 변경(예: 데이터 로드 후) 후에는 **ScrollTrigger.refresh()**를 호출하세요; Vue/Svelte에서는 이는 종종 DOM 업데이트 후 (예: Vue의 nextTick, Svelte의 tick, 또는 비동기 콘텐츠 로드 후)를 의미합니다.
생성 vs 제거 시점
| 생명주기 | 작업 |
|---|---|
| 마운트됨 | gsap.context(scope) 내부에서 트윈과 ScrollTrigger를 생성합니다. |
| 언마운트 / 소멸 | **ctx.revert()**를 호출하여 해당 컨텍스트의 모든 애니메이션과 ScrollTrigger를 제거하고 인라인 스타일을 원래대로 복원합니다. |
컴포넌트의 setup 또는 루트 요소가 존재하기 전에 실행되는 동기 최상위 스크립트에서 GSAP 애니메이션을 생성하지 마세요. onMounted / onMount (또는 이에 상응하는) 시점을 기다려 컨테이너 ref가 DOM에 있도록 하세요.
하지 말아야 할 것
- ❌ 컴포넌트가 마운트되기 전(onMounted 없이 setup 등에서)에 트윈이나 ScrollTrigger를 생성하지 마세요; DOM 노드가 아직 존재하지 않을 수 있습니다.
- ❌ scope 없이 선택자 문자열을 사용하지 마세요 (컨테이너를 gsap.context()의 두 번째 인자로 전달) 선택자가 컴포넌트 외부 요소와 일치하지 않도록 해야 합니다.
- ❌ 정리를 생략하지 마세요; onUnmounted / onMount의 반환에서 항상 **ctx.revert()**를 호출하여 컴포넌트가 소멸될 때 애니메이션과 ScrollTrigger가 제거되도록 해야 합니다.
- ❌ 매 렌더링마다 실행되는 컴포넌트 본문 내에서 플러그인을 등록하지 마세요 (문제가 되지는 않지만 낭비입니다); 앱 수준에서 한 번만 등록하세요.
더 알아보기
- React 관련 패턴을 위한 gsap-react 스킬 (useGSAP, contextSafe).