GSAP(GreenSock Animation Platform)은 자바스크립트로 웹에서 구현할 수 있는 거의 모든 애니메이션을 제어할 수 있는 초고성능 라이브러리입니다. 바닐라 JS, React, Vue 등 어떤 환경에서도 사용 가능하며, CSS 애니메이션보다 세밀한 제어와 타임라인 체이닝이 가능합니다.
[그림 1] DOM 요소를 강력하게 제어하는 전역 gsap 객체와 트윈, 타임라인, 플러그인 생태계
가장 빠르고 간편하게 시작하는 방법입니다. HTML 파일의 <head> 혹은 <body> 하단에 아래 스크립트 태그를 추가하면 즉시 사용할 수 있습니다.
현업 프론트엔드 개발 환경에서는 패키지 매니저를 통해 설치한 뒤 import gsap from 'gsap' 형태로 불러옵니다.
가장 핵심이 되는 객체는 gsap 객체이며, 이 객체의 메서드들을 호출하여 요소를 애니메이션시킵니다.
[그림 1] 대상의 상태 변화 방향을 결정하는 3가지 기본 메서드
현재 CSS에 정의된 요소의 상태를 기준으로 애니메이션이 어떤 방향으로 흐르는지 비교해 보세요.
상황에 따라 알맞은 메서드를 선택하여 애니메이션을 구성합니다.
| 메서드 | 방향 (흐름) | 주요 사용처 및 특징 |
|---|---|---|
gsap.to() |
현재 상태 ➔ 목표 상태 | 버튼 호버, 메뉴 열기 등 가장 기본적이고 많이 쓰이는 애니메이션입니다. |
gsap.from() |
임의의 상태 ➔ 현재 상태 | 요소가 투명도 0에서 1로 나타나거나, 화면 밖에서 날아오는 등장(Intro) 애니메이션에 매우 유용합니다. |
gsap.fromTo() |
시작 상태 ➔ 목표 상태 | CSS에 정의된 원래 스타일을 완전히 무시하고 시작점과 끝점을 강제로 고정할 때 사용합니다. 반복 재생 시 오류가 적어 안정적입니다. |
GSAP 객체 내부에 속성을 작성할 때는 자바스크립트 객체(Object) 문법을 따르므로, 케밥 케이스(background-color)가 아닌 카멜 케이스(backgroundColor)를 사용해야 합니다. 값에 단위(px, %)가 필요할 경우 반드시 따옴표(String)로 묶어주어야 합니다.
트윈 메서드는 보통 (대상, {속성들}) 형태로 작성됩니다.
gsap.to(".box", { x: 100, duration: 1, ease: "power2.out" });
x, y, rotation, scale, opacity, backgroundColor 등 CSS 관련 속성의 목표값을 지정합니다.duration: 애니메이션 진행 시간 (기본값: 0.5초)delay: 시작하기 전 대기 시간 (예: 1을 넣으면 1초 뒤 시작)ease: 가속 및 감속 등 움직임의 느낌을 문자열로 지정합니다.onComplete: 애니메이션이 완전히 끝난 후 실행할 콜백 함수를 지정합니다.
💡 gsap.fromTo의 경우 인자가 3개입니다: (".box", {시작 상태 속성들}, {도착 상태 속성들 + duration 등})
ease 옵션은 애니메이션의 생동감을 결정하는 가장 중요한 요소입니다.
"none" / "linear": 일정한 속도 (로딩바)"power1": 살짝 부드러움"power2": 적당히 부드러움 (가장 많이 씀)"power3": 강한 가감속"power4": 매우 극적인 가감속"back": 목표 지점을 살짝 지나쳤다가 되돌아옴 (탄성 느낌)"elastic": 고무줄처럼 띠용띠용 여러 번 튕김"bounce": 공이 바닥에 떨어져 통통 튀기는 느낌"circ": 둥근 궤적 (물방울 느낌)"expo": 급격한 가속/감속"sine": 사인 곡선 기반의 매우 부드러운 움직임💡 in / out / inOut 꼬리표 붙이기
모든 이징의 뒤에는 시점을 결정하는 꼬리표를 붙일 수 있습니다. (예: "power2.out")
- .in : 천천히 시작해서 끝날 때 빠르게 가속
- .out (기본값) : 빠르게 시작해서 끝날 때 천천히 감속 (UI에서 자연스러움)
- .inOut : 천천히 시작 → 빨라짐 → 천천히 멈춤
<!-- 실습을 위해 GSAP CDN을 포함합니다 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<div class="demo-wrapper">
<div class="track">
<span class="label">to()</span>
<div class="box box1"></div>
</div>
<div class="track">
<span class="label">from()</span>
<div class="box box2"></div>
</div>
<div class="track">
<span class="label">fromTo()</span>
<div class="box box3"></div>
</div>
<button id="playBtn" class="play-btn">애니메이션 재실행</button>
</div>애니메이션이 얼마나 오래 지속될지(duration), 언제 시작할지(delay), 그리고 움직임의 질감은 어떨지(ease)를 제어하는 것은 애니메이션의 퀄리티를 결정짓습니다.
power1.out(점점 느려짐)이며, linear(일정함), bounce, elastic 등 다양한 이징 함수를 제공합니다.
"elastic" 이징은 고무줄처럼 튕기는 효과를 줍니다. 괄호 안에 두 개의 숫자를 넣어 탄성의 강도와 튕기는 횟수를 디테일하게 조절할 수 있습니다.
예: ease: "elastic.out(1, 0.3)"
// 크고 부드럽게 튕기는 효과
ease: "elastic.out(1.5, 0.5)"
// 바르르 떨리는 효과
ease: "elastic.out(1, 0.1)"
<!-- 실습을 위해 GSAP CDN을 포함합니다 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<div class="demo-wrapper">
<div class="track">
<span class="label">none</span>
<div class="ball ball-linear"></div>
</div>
<div class="track">
<span class="label">bounce</span>
<div class="ball ball-bounce"></div>
</div>
<div class="track">
<span class="label">elastic</span>
<div class="ball ball-elastic"></div>
</div>
<button id="playBtn" class="play-btn">타이밍 비교 실행 (▶)</button>
</div>stagger 속성 하나면 마치 도미노가 쓰러지듯 시간차를 두고 애니메이션을 실행할 수 있습니다. 복잡한 for 반복문 없이 단 한 줄로 마법을 부려보세요.
[그림 1] 도미노처럼 순차적으로 실행되는 Stagger 애니메이션의 시각적 메타포
모든 요소가 동시에 움직일 때와 stagger를 적용하여 순차적으로 움직일 때의 타임라인 차이입니다.
stagger 속성은 단순 숫자(시간) 외에도 객체 형태로 세밀한 옵션을 제공합니다.
| 속성 | 사용 예시 | 설명 |
|---|---|---|
amount |
stagger: { amount: 1 } |
각 요소 사이의 간격이 아닌, 전체 애니메이션이 완료되는 총 시간을 배분합니다. |
each |
stagger: { each: 0.1 } |
단순 숫자 stagger: 0.1과 동일합니다. 각 요소별 간격을 지정합니다. |
from |
stagger: { from: "center" } |
애니메이션이 시작되는 기준점을 지정합니다. "start", "center", "end", "edges", "random". |
grid |
stagger: { grid: [3,3] } |
2D 그리드 방사형으로 퍼지는 효과를 줍니다. |
<!-- 실습을 위해 GSAP CDN을 포함합니다 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<div class="demo-wrapper">
<div class="controls">
<button id="btn-play">도미노 재생 ▶</button>
<button id="btn-reset">초기화 ↺</button>
</div>
<div class="grid">
<div class="card">1</div>
<div class="card">2</div>
<div class="card">3</div>
<div class="card">4</div>
<div class="card">5</div>
<div class="card">6</div>
</div>
</div>GSAP로 만든 애니메이션은 비디오 플레이어처럼 play(), pause(), reverse() 등의 메서드를 통해 언제든지 멈추거나 뒤로 감을 수 있습니다. 또한, 애니메이션이 시작하거나 완전히 종료되었을 때 특정 자바스크립트 함수를 실행하고 싶다면 onComplete, onStart 같은 콜백(Callback) 함수를 연결할 수 있습니다.
[그림 1] 애니메이션 상태 제어와 콜백 실행 시점의 시각적 메타포
애니메이션을 변수에 담으면 아래와 같은 메서드를 호출하여 재생 상태를 관리할 수 있습니다.
| 메서드 | 설명 (인자 활용법) |
|---|---|
play(time?) |
애니메이션을 정방향으로 재생합니다. * 선택 인자로 시간(초)을 넣으면 해당 시점부터 재생합니다. 예: play(2) (2초 지점부터 시작)
|
pause(time?) |
진행 중인 애니메이션을 일시 정지합니다. * 특정 시점에서 정지시키고 싶다면 pause(1.5)와 같이 시간을 지정할 수 있습니다. (초기화 할 때 pause(0) 활용)
|
reverse(time?) |
애니메이션을 역방향으로 재생합니다. (되감기) * 특정 시점부터 되감고 싶다면 reverse(2) 등 기준 시간을 넣을 수 있습니다.
|
restart(includeDelay?) |
처음부터 다시 재생합니다. * 첫 번째 인자로 true를 넣으면 처음에 설정한 delay(대기 시간)까지 다시 기다렸다가 재생합니다. (기본값: false, 즉시 재시작)
|
애니메이션의 생명주기(Lifecycle)에 맞춰 원하는 코드를 실행할 수 있습니다.
시작될 때 1회 실행
매 프레임마다 실행
정방향 종료 후 실행
되감기 완료 후 실행
<!-- 실습을 위해 GSAP CDN을 포함합니다 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<div class="playground">
<div class="box"></div>
<div class="controls">
<button id="playBtn" class="btn play">▶ Play</button>
<button id="pauseBtn" class="btn pause">❚❚ Pause</button>
<button id="reverseBtn" class="btn reverse">◀ Reverse</button>
<button id="restartBtn" class="btn restart">↺ Restart</button>
</div>
<div id="status" class="status-msg">대기 중...</div>
</div>delay 속성을 일일이 계산해야 할까요?
[그림 1] 순차적으로 이어지는 타임라인의 시각적 메타포
왜 타임라인을 써야 할까요? 타임라인이 없을 때의 고통(수동 계산)과 있을 때의 편안함을 비교해 보세요.
타임라인 객체는 기본 트윈(Tween)과 동일하게 to(), from()을 지원하며, 전체 흐름을 일괄 제어할 수 있습니다.
| 메서드 | 설명 |
|---|---|
gsap.timeline() |
빈 타임라인 객체를 생성합니다. 여기에 애니메이션을 줄줄이 붙일 수 있습니다. |
tl.to() / tl.from() |
타임라인에 요소를 추가합니다. 코드가 체이닝된 순서대로 차례대로 실행됩니다. |
tl.pause() / tl.play() |
타임라인에 연결된 모든 애니메이션 시퀀스를 통째로 정지하거나 재생합니다. |
타임라인을 기껏 생성(const tl = gsap.timeline();)해놓고, 정작 애니메이션을 작성할 때 gsap.to()를 사용하면 타임라인에 등록되지 않아 개별적으로 즉시 동시 실행되어 버립니다. 타임라인에 엮을 때는 반드시 tl.to() 처럼 타임라인 변수명에 체이닝해야 함을 잊지 마세요!
<!-- 실습을 위해 GSAP CDN을 포함합니다 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<div class="timeline-demo">
<button id="startBtn" class="play-btn">▶ 애니메이션 시퀀스 실행</button>
<div class="track">
<div class="box box1">1</div>
<div class="box box2">2</div>
<div class="box box3">3</div>
</div>
</div>
[그림 1] 포지션 파라미터에 따른 타임라인 오프셋 메타포
가장 자주 쓰이는 4가지 기호의 시각적인 차이를 확인해보세요.
포지션 파라미터는 to(), from(), fromTo() 메서드의 마지막 인자로 전달됩니다.
| 파라미터 | 동작 원리 및 설명 |
|---|---|
(생략) 또는 ">" |
기본 상태. 바로 앞 애니메이션이 완전히 끝나는 즉시 실행됩니다. |
"<" |
동시 실행. 가장 최근에 추가된 애니메이션의 시작점에 맞추어 같이 출발합니다. |
"-=초" |
겹치기 (Overlap). 앞 애니메이션이 끝나기 지정된 초(Second) 만큼 전에 먼저 출발합니다. (예: "-=0.5") |
"+=초" |
지연 (Gap). 앞 애니메이션이 끝난 후 지정된 초 만큼 기다렸다가 출발합니다. (예: "+=1") |
절대값 (예: 2) |
문자열이 아닌 단순 숫자를 넣으면, 타임라인이 시작된 지 정확히 해당 초(Second) 지점에서 실행됩니다. |
숫자 2처럼 절대 시간을 입력하면 유지보수가 어려워집니다. 중간에 다른 애니메이션이 추가되거나 길이가 변경되어도 유연하게 대응하려면 "-=0.5"나 "<" 같은 상대적인 위치 기호를 사용하는 것이 타임라인의 장점을 극대화하는 길입니다.
<!-- 실습을 위해 GSAP CDN을 포함합니다 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<div class="demo-wrapper">
<button id="playBtn" class="play-btn">▶ 시퀀스 재생</button>
<div class="track">
<div class="item item1">Item 1</div>
<div class="item item2">Item 2</div>
<div class="item item3">Item 3</div>
<div class="item item4">Item 4</div>
</div>
</div>timeScale()과, 현재 진행 상황을 거꾸로 되감는 reverse() 기능은 인터랙티브한 UI를 구성할 때 코드를 획기적으로 줄여주는 GSAP 타임라인만의 핵심 무기입니다.
[그림 1] 마스터 타임라인을 통해 하위 시퀀스를 일괄 제어하는 개념
타임라인 객체 하나만 조작하면 내부의 모든 애니메이션이 함께 영향을 받습니다.
| 메서드 | 설명 및 동작 방식 |
|---|---|
tl.timeScale(multiplier) |
타임라인의 재생 속도를 배수로 조절합니다. 1은 정상, 2는 두 배 빠름, 0.5는 절반 속도(슬로우 모션)입니다. |
tl.reverse() |
현재 시점에서부터 애니메이션을 거꾸로 역재생합니다. 모달 창이나 메뉴를 닫을 때 닫기 애니메이션을 따로 만들 필요 없이 유용합니다. |
tl.progress(value) |
애니메이션의 진행도를 0(시작)에서 1(끝) 사이의 값으로 강제 세팅합니다. |
메서드를 체이닝하여 tl.timeScale(2).play() 처럼 작성하면 코드가 매우 간결해집니다. 역재생을 할 때는 역재생 속도도 timeScale의 영향을 받으므로, 정상 속도로 되감고 싶다면 항상 tl.timeScale(1).reverse()를 습관화하는 것이 좋습니다.
<!-- 실습을 위해 GSAP CDN을 포함합니다 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<div class="master-wrapper">
<div class="controls">
<button id="btn-play" class="btn play">▶ 기본 재생</button>
<button id="btn-fast" class="btn fast">⏩ 2배속 재생</button>
<button id="btn-slow" class="btn slow">🐢 0.5배속 재생</button>
<button id="btn-reverse" class="btn reverse">⏪ 역재생 (되감기)</button>
</div>
<div class="track">
<div class="circle">1</div>
<div class="circle">2</div>
<div class="circle">3</div>
<div class="circle">4</div>
</div>
</div>ScrollTrigger는 GSAP의 공식 플러그인으로, 브라우저의 스크롤 위치를 감지하여 애니메이션을 완벽하게 제어하는 프론트엔드 생태계 최강의 도구입니다.
[그림 1] 브라우저 스크롤 이벤트와 타임라인 애니메이션을 완벽하게 동기화하는 ScrollTrigger 시스템
브라우저 창(Viewport)의 특정 지점과 감시 대상 요소의 지점이 만날 때 애니메이션이 실행되는 시각적 교차 원리입니다.
강력한 플러그인인 만큼, 사용하기 전에 확실하게 알아야 할 기본 원칙 세 가지가 있습니다.
| 규칙 | 코드 예시 | 설명 |
|---|---|---|
1. 라이브러리 추가 |
<script src="...ScrollTrigger.min.js"> | GSAP 코어 외에 스크롤트리거 플러그인 js 파일을 별도로 로드해야 합니다. |
2. 플러그인 등록 |
gsap.registerPlugin(ScrollTrigger); | 자바스크립트 최상단에 단 한 번 선언하여 GSAP 엔진에 플러그인을 인식시킵니다. |
3. 트리거 할당 |
scrollTrigger: ".box" | 어떤 요소가 화면에 나타날 때 애니메이션을 시작할지 기준점(Trigger Element)을 반드시 잡아주어야 합니다. |
일반적인 웹사이트에서는 브라우저 창 전체의 스크롤을 감지합니다. 하지만 여기서는 Iframe 내부에 실습 화면이 있으므로, scroller: ".scroll-container"와 같이 강제로 스크롤 영역을 직접 지정해주어야 스크롤트리거가 제대로 감지됩니다.
<script>
// [시스템 핫픽스] Iframe Resizer가 내부 스크롤 요소를 측정하지 못하게 차단
const origQSA = document.querySelectorAll;
document.querySelectorAll = function(selector) {
const elements = origQSA.call(this, selector);
if (selector === '*') {
return Array.from(elements).filter(el => !el.closest('.scroll-container'));
}
return elements;
};
</script>
<!-- 1. 필수 라이브러리 로드 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>
<div class="scroll-container">
<div class="intro-section">
<h2 style="margin:0; margin-bottom:1rem;">아래로 스크롤 해보세요 👇</h2>
<div class="mouse-icon"></div>
</div>
<div class="trigger-section">
<div class="box">마법 등장!</div>
</div>
</div>start와 end 속성으로 픽셀(px)이나 퍼센트(%) 단위로 매우 세밀하게 조율할 수 있습니다. 눈에 보이지 않는 교차점을 머릿속으로만 계산하기는 어렵기 때문에, 개발 단계에서는 반드시 markers: true 옵션을 켜서 기준선을 시각적으로 확인하며 작업해야 합니다.
[그림 1] 스크롤트리거의 Start와 End 교차점을 시각적으로 보여주는 마커(Markers) 시스템
모든 기준점은 "요소의 기준점, 뷰포트의 기준점" 순서로 작성되는 띄어쓰기 된 두 개의 단어로 이루어집니다.
| 속성 | 작성 예시 | 설명 |
|---|---|---|
start |
"top center" "top 80%" |
애니메이션이 시작되는 교차점입니다. 예: 요소의 상단(top)이 화면의 중앙(center)에 올 때. |
end |
"bottom top" "+=300px" |
애니메이션이 종료되는 교차점입니다. 예: 요소의 하단(bottom)이 화면의 최상단(top)에 닿을 때. |
markers |
true | start, end 위치를 디버깅할 수 있도록 화면 우측에 기준선을 표시합니다. |
markers: true는 화면 우측에 start, end 등의 텍스트 기준선을 렌더링하는 아주 무거운 작업입니다. 오직 개발 및 디버깅용으로만 사용하시고, 실제 라이브 환경에 배포할 때는 반드시 삭제하거나 false로 처리해야 심각한 브라우저 렌더링 성능 저하를 피할 수 있습니다.
<script>
// [시스템 핫픽스] Iframe Resizer가 내부 스크롤 요소를 측정하지 못하게 차단
const origQSA = document.querySelectorAll;
document.querySelectorAll = function(selector) {
const elements = origQSA.call(this, selector);
if (selector === '*') {
return Array.from(elements).filter(el => !el.closest('.scroll-container'));
}
return elements;
};
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>
<div class="scroll-container">
<div class="section empty">
<h3>스크롤을 아래로 내려보세요 👇</h3>
</div>
<div class="section trigger-zone">
<div class="target-box">START!</div>
</div>
<div class="section empty">
<h3>더 내려보세요 👇</h3>
</div>
</div>scrub 옵션을 사용합니다.pin: true를 사용합니다. 이 두 가지를 결합하면 마치 Apple 제품 소개 페이지 같은 역동적인 스토리텔링 웹 페이지를 만들 수 있습니다.
[그림 1] 스크롤바와 타임라인을 동기화하는 Scrub 방식과 뷰포트를 고정하는 Pin 동작 원리
| 속성 | 작성 예시 | 설명 |
|---|---|---|
scrub |
scrub: true scrub: 1 |
true: 스크롤바와 애니메이션 진행률이 완전히 1:1로 실시간 동기화됩니다. 숫자(초): 스크롤이 멈춘 후 지정된 시간(초)만큼 지연되며 부드럽게(Smooth) 애니메이션이 따라붙습니다. |
pin |
pin: true pin: ".container" |
지정된 트리거(또는 직접 입력한 요소)를 애니메이션이 종료(end)될 때까지 현재 화면에 고정(position: fixed 처럼 동작)합니다.
|
end |
end: "+=1000" |
일반적으로 Pin을 사용할 때는 특정 지점 대신 +=숫자 형식을 사용해 "시작점으로부터 n픽셀을 스크롤하는 동안" 애니메이션을 유지시킵니다.
|
scrub: true를 쓰면 마우스를 멈출 때 애니메이션도 즉각적으로 딱딱하게 멈춥니다. 하지만 scrub: 1처럼 1초 딜레이를 주면, 사용자가 마우스 스크롤을 멈춰도 애니메이션이 1초간 관성처럼 부드럽게 이어지면서 감속하여 멈추기 때문에 훨씬 더 고급스러운 UI UX를 제공할 수 있습니다.
<script>
// [시스템 핫픽스] Iframe Resizer가 내부 스크롤 요소를 측정하지 못하게 차단
const origQSA = document.querySelectorAll;
document.querySelectorAll = function(selector) {
const elements = origQSA.call(this, selector);
if (selector === '*') {
return Array.from(elements).filter(el => !el.closest('.scroll-container'));
}
return elements;
};
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>
<div class="scroll-container">
<div class="panel">⬇️ 스크롤 시작 ⬇️</div>
<div class="panel pin-section">
<div class="ghost">👻</div>
<div class="loading-bar">
<div class="progress"></div>
</div>
</div>
<div class="panel">⬆️ 스크롤 끝 ⬆️</div>
</div>onEnter) 애니메이션이 재생(Play)되었습니다. 스크롤을 더 내려서 요소가 화면 밖으로 완전히 벗어나면(onLeave) 어떻게 될까요? 스크롤을 다시 올려서 요소가 보일 때(onEnterBack)는요?toggleActions는 스크롤을 내리거나 올릴 때 발생하는 이 4가지 진입/이탈 생명주기 이벤트에 대해 애니메이션을 어떻게 처리할지를 매우 간결한 문자열 하나로 정의합니다.
[그림 1] 스크롤 방향(위/아래)과 뷰포트 교차 상태에 따라 발동하는 4가지 생명주기
toggleActions: "① ② ③ ④" 와 같이 반드시 4개의 단어를 띄어쓰기로 구분하여 작성해야 합니다. 사용할 수 있는 명령어는 play, pause, resume, reverse, restart, reset, complete, none 입니다.
| 순서 | 이벤트명 | 발동 조건 | 설명 |
|---|---|---|---|
| ① 1번째 | onEnter |
스크롤 다운 ⬇️ (요소가 진입할 때) |
뷰포트의 start 지점을 지나 아래로 스크롤 될 때 발생합니다. 일반적으로 play를 사용합니다. |
| ② 2번째 | onLeave |
스크롤 다운 ⬇️ (요소가 떠날 때) |
뷰포트의 end 지점을 지나 더 아래로 스크롤 될 때 발생합니다. none이나 pause 등을 씁니다. |
| ③ 3번째 | onEnterBack |
스크롤 업 ⬆️ (다시 진입할 때) |
뷰포트의 end 지점을 지나 위로 다시 올라올 때 발생합니다. reverse 나 resume을 주로 씁니다. |
| ④ 4번째 | onLeaveBack |
스크롤 업 ⬆️ (다시 떠날 때) |
뷰포트의 start 지점을 지나 완전히 위로 벗어날 때 발생합니다. 초기화(reset)를 주로 씁니다. |
개발자가 toggleActions를 생략할 경우, GSAP의 기본값은 "play none none none" 입니다. 즉, 최초 화면에 나타날 때 딱 한 번 재생되고 그 이후에는 스크롤을 어떻게 하든 아무 일도 일어나지 않는 것이 기본 동작입니다.
<script>
// [시스템 핫픽스] Iframe Resizer가 내부 스크롤 요소를 측정하지 못하게 차단
const origQSA = document.querySelectorAll;
document.querySelectorAll = function(selector) {
const elements = origQSA.call(this, selector);
if (selector === '*') {
return Array.from(elements).filter(el => !el.closest('.scroll-container'));
}
return elements;
};
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>
<div class="scroll-container">
<div class="space">스크롤을 내려 박스를 만나보세요 👇</div>
<div class="trigger-zone">
<div class="box">PLAY</div>
</div>
<div class="space">스크롤을 위아래로 움직여 상태 변화를 확인하세요 🔄</div>
</div>브라우저의 기본 스크롤은 운영체제나 마우스에 따라 딱딱하게 끊기거나 속도가 일정하지 않습니다. 이로 인해 ScrollTrigger 애니메이션이 뚝뚝 끊겨 보이는 현상(Jank)이 발생할 수 있습니다. Lenis는 최신 브라우저 환경에 최적화된 부드러운 스크롤(Smooth Scroll) 라이브러리이며, GSAP과 공식적으로 가장 잘 맞는 파트너입니다.
Lenis가 스크롤 값을 갱신할 때마다 ScrollTrigger.update를 호출하여 GSAP에게 스크롤 위치가 변했음을 알리고, GSAP의 내부 타이머(gsap.ticker)가 매 프레임마다 Lenis의 스크롤 위치를 부드럽게 계산하도록 연결합니다.
<!-- 1. GSAP & ScrollTrigger 로드 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>
<!-- 2. Lenis 부드러운 스크롤 라이브러리 로드 -->
<script src="https://unpkg.com/@studio-freight/lenis@1.0.34/dist/lenis.min.js"></script>
<div class="scroll-container">
<div class="scroll-content">
<div class="intro-section">
<h2 style="margin:0; margin-bottom:1rem;">마우스를 굴려보세요 👇</h2>
<p style="opacity: 0.7; font-size: 0.9rem;">스크롤이 평소보다 훨씬 부드럽게 움직입니다.</p>
</div>
<div class="trigger-section">
<div class="box">부드러운<br>등장!</div>
</div>
<div class="intro-section">
<p style="opacity: 0.7; font-size: 0.9rem;">부드러운 스크롤 덕분에 애니메이션도 매끄럽게 연결됩니다.</p>
</div>
</div>
</div>
제목이나 문장 전체가 통째로 나타나는 것보다, 글자 하나하나가 순차적으로 나타나면 훨씬 더 생동감 넘치는 디자인을 만들 수 있습니다. SplitType 같은 라이브러리를 사용해 텍스트를 글자(chars) 단위로 쪼개고, GSAP의 stagger(시차) 속성을 결합하면 타이핑 효과나 파도타기 효과를 단 몇 줄의 코드로 완벽하게 구현할 수 있습니다.
1. SplitType 초기화: new SplitType('.title', { types: 'chars' })를 통해 각 글자를 <span> 태그로 감쌉니다.
2. CSS 준비: 부모 컨테이너에 clip-path나 overflow: hidden을 주어 글자가 바닥에서 올라오는 효과를 극대화합니다.
3. stagger 적용: gsap.from(myText.chars, { stagger: 0.05, y: 100 })을 통해 각 글자가 0.05초 간격으로 시차를 두고 애니메이션을 시작하게 만듭니다.
<!-- 1. GSAP 라이브러리 로드 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<!-- 2. 무료 텍스트 분할 라이브러리 로드 -->
<script src="https://unpkg.com/split-type"></script>
<div class="container">
<!-- 마스크 역할을 할 컨테이너 (클리핑 영역) -->
<div class="text-mask">
<h1 class="title">Split Type Magic</h1>
</div>
<p class="subtitle">GSAP Stagger 효과와 결합하여 글자가 순차적으로 떠오릅니다.</p>
<button class="replay-btn" onclick="playAnimation()">다시 보기 🔄</button>
</div>
React(가상 DOM)에서 GSAP(실제 DOM 조작)을 사용할 때는 컴포넌트가 언마운트될 때 진행 중인 애니메이션과 ScrollTrigger를 정리(Cleanup)해 주어야 메모리 누수와 버그를 막을 수 있습니다.
기존에는 useEffect와 gsap.context()를 묶어서 복잡하게 처리했지만, GSAP 공식 React 훅인 @gsap/react (useGSAP)를 사용하면 내부적으로 스코프 격리와 클린업을 완벽하게 자동 처리해 줍니다.
useGSAP(() => { ... }, { scope: containerRef, dependencies: [state] });
scope 지정: 컴포넌트 외부의 동명이인 요소('.box' 등)를 건드리지 않도록 안전하게 격리합니다.
자동 Cleanup: 컴포넌트가 사라지거나 재랜더링될 때 애니메이션을 깔끔하게 폐기(revert)합니다.
import React, { useRef, useState } from 'react';
import gsap from 'gsap';
import { useGSAP } from '@gsap/react'; // GSAP 공식 React 훅
// 플러그인 등록 (Next.js/React 공통)
gsap.registerPlugin(useGSAP);
export default function App() {
const containerRef = useRef(null);
const [clickCount, setClickCount] = useState(0);
// 💡 useEffect 대신 useGSAP 사용!
useGSAP(() => {
// 1. scope 옵션 덕분에 containerRef 내부의 '.box'만 선택됩니다.
// 2. 컴포넌트 언마운트 시 자동 Cleanup (revert) 됩니다.
gsap.from('.box', {
y: 50,
opacity: 0,
stagger: 0.1,
duration: 1,
ease: 'back.out(1.5)'
});
// 버튼을 클릭할 때마다 텍스트 애니메이션 (dependencies 배열 활용)
if (clickCount > 0) {
gsap.fromTo('.counter',
{ scale: 1.5, color: '#f59e0b' },
{ scale: 1, color: '#94a3b8', duration: 0.5, clearProps: 'all' }
);
}
}, {
scope: containerRef,
dependencies: [clickCount] // clickCount가 변할 때마다 재실행 및 자동 정리
});
return (
<div className="container">
<div ref={containerRef} className="card">
<h2 className="title">useGSAP in React</h2>
<div className="box-container">
<div className="box"></div>
<div className="box"></div>
<div className="box"></div>
</div>
<div className="controls">
<button
className="action-btn"
onClick={() => setClickCount(c => c + 1)}
>
클릭하여 의존성 재실행 🚀
</button>
<p className="counter">클릭 횟수: {clickCount}</p>
</div>
</div>
</div>
);
}