✨ 모던 웹 애니메이션의 표준, GSAP
GSAP(GreenSock Animation Platform)은 자바스크립트로 웹에서 구현할 수 있는 거의 모든 애니메이션을 제어할 수 있는 초고성능 라이브러리입니다. 바닐라 JS, React, Vue 등 어떤 환경에서도 사용 가능하며, CSS 애니메이션보다 세밀한 제어와 타임라인 체이닝이 가능합니다.
[그림 1] DOM 요소를 강력하게 제어하는 전역 gsap 객체와 트윈, 타임라인, 플러그인 생태계
🚀 설치 방법
1. CDN을 통한 설치
가장 빠르고 간편하게 시작하는 방법입니다. HTML 파일의 <head> 혹은 <body> 하단에 아래 스크립트 태그를 추가하면 즉시 사용할 수 있습니다.
2. NPM 모듈 패키지 매니저 환경 (React, Vue, Next.js 등)
현업 프론트엔드 개발 환경에서는 패키지 매니저를 통해 설치한 뒤 import gsap from 'gsap' 형태로 불러옵니다.
가장 핵심이 되는 객체는 gsap 객체이며, 이 객체의 메서드들을 호출하여 요소를 애니메이션시킵니다.
핵심 3대장: to(), from(), fromTo()
복잡한 애니메이션도 결국 이 3가지 기본 메서드의 조합으로 만들어집니다.
[그림 1] 대상의 상태 변화 방향을 결정하는 3가지 기본 메서드
1. 변화의 흐름 (시각적 이해)
현재 CSS에 정의된 요소의 상태를 기준으로 애니메이션이 어떤 방향으로 흐르는지 비교해 보세요.
2. 트윈(Tween) 메서드 핵심 요약
상황에 따라 알맞은 메서드를 선택하여 애니메이션을 구성합니다.
| 메서드 | 방향 (흐름) | 주요 사용처 및 특징 |
|---|---|---|
gsap.to() |
현재 상태 ➔ 목표 상태 | 버튼 호버, 메뉴 열기 등 가장 기본적이고 많이 쓰이는 애니메이션입니다. |
gsap.from() |
임의의 상태 ➔ 현재 상태 | 요소가 투명도 0에서 1로 나타나거나, 화면 밖에서 날아오는 등장(Intro) 애니메이션에 매우 유용합니다. |
gsap.fromTo() |
시작 상태 ➔ 목표 상태 | CSS에 정의된 원래 스타일을 완전히 무시하고 시작점과 끝점을 강제로 고정할 때 사용합니다. 반복 재생 시 오류가 적어 안정적입니다. |
GSAP 객체 내부에 속성을 작성할 때는 자바스크립트 객체(Object) 문법을 따르므로, 케밥 케이스(background-color)가 아닌 카멜 케이스(backgroundColor)를 사용해야 합니다. 값에 단위(px, %)가 필요할 경우 반드시 따옴표(String)로 묶어주어야 합니다.
<!-- 실습을 위해 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>시간과 움직임의 질감 (Timing & Easing)
GSAP는 매우 세밀하고 직관적인 시간 제어 속성(
duration, delay)과 물리 법칙 기반의 다채로운 가속도 곡선(ease)을 제공합니다.
[그림 1] 가속도 곡선에 따라 애니메이션의 물리적 타격감과 질감이 다르게 표현됩니다.
1. 이징(Ease) 곡선의 시각적 이해
X축은 시간(Time), Y축은 애니메이션의 진행도(Progress)를 나타냅니다.
2. 타이밍 제어 핵심 속성
트윈 객체의 두 번째 인자(Options) 안에서 설정합니다.
| 속성명 | 단위/타입 | 설명 및 기본값 |
|---|---|---|
duration |
초 (Number) | 애니메이션이 재생되는 총 시간입니다. 명시하지 않으면 기본값인 0.5초가 적용됩니다. |
delay |
초 (Number) | 애니메이션이 시작하기 전까지 대기하는 시간입니다. 여러 요소가 순차적으로 나타날 때 유용합니다. |
ease |
문자열 (String) | 가속도 질감 함수입니다. 기본값은 "power1.out"이며, 끝날 때 자연스럽게 감속합니다. "bounce.out", "elastic.out" 등 매우 다양합니다. |
<!-- 실습을 위해 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)
게시판 목록, 갤러리 이미지, 메뉴 아이템 등 여러 개의 요소가 있을 때
stagger 속성 하나면 마치 도미노가 쓰러지듯 시간차를 두고 애니메이션을 실행할 수 있습니다. 복잡한 for 반복문 없이 단 한 줄로 마법을 부려보세요.
[그림 1] 도미노처럼 순차적으로 실행되는 Stagger 애니메이션의 시각적 메타포
1. Stagger (시간차 애니메이션) 워크플로우 비교
모든 요소가 동시에 움직일 때와 stagger를 적용하여 순차적으로 움직일 때의 타임라인 차이입니다.
2. Stagger 고급 속성 (Advanced Options)
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>애니메이션 상태 제어와 콜백 함수 (Control & Callbacks)
GSAP로 만든 애니메이션은 비디오 플레이어처럼
play(), pause(), reverse() 등의 메서드를 통해 언제든지 멈추거나 뒤로 감을 수 있습니다. 또한, 애니메이션이 시작하거나 완전히 종료되었을 때 특정 자바스크립트 함수를 실행하고 싶다면 onComplete, onStart 같은 콜백(Callback) 함수를 연결할 수 있습니다.
[그림 1] 재생 상태를 제어하는 컨트롤러와 애니메이션 종료 시 발생하는 콜백 이벤트
1. 주요 제어 메서드 (Control Methods)
애니메이션을 변수에 담으면 아래와 같은 메서드를 호출하여 재생 상태를 관리할 수 있습니다.
| 메서드 | 설명 |
|---|---|
play() |
애니메이션을 정방향으로 재생합니다. |
pause() |
진행 중인 애니메이션을 일시 정지합니다. |
reverse() |
애니메이션을 역방향으로 재생합니다. (되감기) |
restart() |
처음부터 다시 재생합니다. |
2. 콜백 함수 (Callbacks)
애니메이션의 생명주기(Lifecycle)에 맞춰 원하는 코드를 실행할 수 있습니다.
애니메이션이 시작될 때 1회 실행
애니메이션이 진행되는 매 프레임마다 실행
애니메이션이 완전히 종료된 후 실행
<style>
.box { width: 100px; height: 100px; background: #8b5cf6; border-radius: 12px; }
.controls { margin-top: 15px; }
button { padding: 8px 16px; margin-right: 5px; cursor: pointer; }
</style>
<div class="box"></div>
<div class="controls">
<button id="play">Play</button>
<button id="pause">Pause</button>
<button id="reverse">Reverse</button>
<button id="restart">Restart</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script>
// 변수에 애니메이션 저장 (초기엔 일시정지 상태로)
const tween = gsap.to('.box', {
x: 300,
rotation: 360,
duration: 2,
paused: true,
onComplete: () => alert("도착 완료!")
});
document.querySelector('#play').onclick = () => tween.play();
document.querySelector('#pause').onclick = () => tween.pause();
document.querySelector('#reverse').onclick = () => tween.reverse();
document.querySelector('#restart').onclick = () => tween.restart();
</script>🎬 복잡한 시퀀스를 우아하게 엮어내는 타임라인
애니메이션 A가 끝나고 애니메이션 B가 시작되고, 다시 C가 시작되게 하려면 delay를 일일이 계산해야 할까요? 타임라인(Timeline)을 사용하면 이 과정이 매우 직관적이고 쉬워집니다.
const tl = gsap.timeline()으로 타임라인 객체를 생성한 후, tl.to().to() 형태로 메서드를 체이닝(Chaining)하면 코드 순서대로 애니메이션이 꼬리에 꼬리를 물고 실행됩니다.
<style>
.box { width: 80px; height: 80px; background: #ec4899; margin: 5px; opacity: 0;}
</style>
<div class="box box1">1</div>
<div class="box box2">2</div>
<div class="box box3">3</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script>
// 타임라인 생성
const tl = gsap.timeline();
// 작성한 순서대로 순차 실행됩니다! delay 계산이 필요 없습니다.
tl.to('.box1', { x: 100, opacity: 1, duration: 1 })
.to('.box2', { x: 100, opacity: 1, duration: 1 })
.to('.box3', { x: 100, opacity: 1, duration: 1 });
</script>⏳ 애니메이션 겹치기와 타이밍 조절
타임라인 체이닝을 사용할 때, 이전 애니메이션이 끝나기 전에 다음 애니메이션을 시작하게 하거나 동시에 실행하고 싶다면 포지션 파라미터(Position Parameter)를 사용합니다.
"<": 이전 애니메이션과 동시에 시작">": 이전 애니메이션이 끝난 직후 (기본값과 동일)"+=1": 이전 애니메이션이 끝난 후 1초 뒤에 시작"-=0.5": 이전 애니메이션이 끝나기 0.5초 전에 겹쳐서 시작
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<style>
.item { width: 100px; height: 40px; background: #14b8a6; margin: 5px; }
</style>
<div class="item i1">Item 1</div>
<div class="item i2">Item 2</div>
<div class="item i3">Item 3</div>
<script>
const tl = gsap.timeline();
tl.to('.i1', { x: 200, duration: 1 })
// i1이 끝나기 0.5초 전에 i2 시작 (겹침)
.to('.i2', { x: 200, duration: 1 }, "-=0.5")
// i2와 정확히 동시에 i3 시작
.to('.i3', { x: 200, duration: 1 }, "<");
</script>⏪ 타임라인 통째로 조종하기
타임라인으로 수십 개의 애니메이션을 묶어두면, 그 덩어리 전체를 하나의 비디오테이프처럼 다룰 수 있습니다.
전체 속도를 2배속, 0.5배속으로 조절하는 timeScale(), 전체 애니메이션을 거꾸로 되감는 reverse() 기능은 복잡한 트랜지션 효과를 만들 때 코드를 절반으로 줄여줍니다.
<style>
.circle { width: 50px; height: 50px; background: #f59e0b; border-radius: 50%; display: inline-block; }
</style>
<button id="btn-fast">2배속 Play</button>
<button id="btn-reverse">Reverse (되감기)</button>
<div style="margin-top:20px;">
<div class="circle"></div><div class="circle"></div><div class="circle"></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script>
const tl = gsap.timeline({ paused: true });
// 3개의 동그라미가 차례대로 커지는 타임라인
tl.to('.circle', { scale: 1.5, y: 30, duration: 1, stagger: 0.3 });
document.querySelector('#btn-fast').onclick = () => {
tl.timeScale(2).play(); // 2배 빠르게 재생
};
document.querySelector('#btn-reverse').onclick = () => {
tl.timeScale(1).reverse(); // 정상 속도로 거꾸로 되감기
};
</script>스크롤 인터랙션의 핵심: ScrollTrigger 입문
Apple 사이트나 화려한 프로모션 페이지에서 스크롤을 내릴 때마다 요소들이 튀어나오는 효과를 본 적이 있나요?
ScrollTrigger는 GSAP의 공식 플러그인으로, 스크롤 위치를 감지하여 애니메이션을 완벽하게 제어하는 프론트엔드 생태계 최강의 도구입니다.
[그림 1] 브라우저 스크롤 이벤트와 타임라인 애니메이션을 완벽하게 동기화하는 ScrollTrigger 시스템
1. 스크롤트리거(ScrollTrigger) 뷰포트 교차 원리
브라우저 창(Viewport)의 특정 지점과 감시 대상 요소의 지점이 만날 때 애니메이션이 실행되는 시각적 원리입니다.
📜 ScrollTrigger 3대 규칙
강력한 만큼 사용 전 확실하게 알아야 할 기본 원칙이 있습니다. 플러그인을 활용하기 위해서는 가장 먼저 registerPlugin을 선언해야 합니다.
| 규칙 | 코드 예시 | 설명 |
|---|---|---|
| 1. 라이브러리 추가 | <script src="...ScrollTrigger.min.js"> | GSAP 코어 외에 플러그인 파일을 추가로 로드해야 합니다. |
| 2. 플러그인 등록 | gsap.registerPlugin(ScrollTrigger) | JS 최상단에 단 한 번 선언하여 엔진에 플러그인을 인식시킵니다. |
| 3. 트리거 할당 | scrollTrigger: ".box" | 어떤 요소가 화면에 나타날 때 애니메이션을 시작할지 기준점을 잡아줍니다. |
<!-- 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>아래로 스크롤 해보세요 👇</h2>
<div class="mouse-icon"></div>
</div>
<div class="trigger-section">
<div class="box">마법 등장!</div>
</div>
</div>📏 어디서 시작하고 어디서 끝날 것인가?
ScrollTrigger의 동작 원리는 트리거 요소의 특정 지점(예: 요소의 맨 위)과 브라우저 화면(뷰포트)의 특정 지점(예: 화면의 가운데)이 교차할 때 발동하는 것입니다.
이 교차점은 start와 end 속성으로 세밀하게 설정할 수 있으며, 개발 단계에서는 markers: true 옵션을 켜서 기준선이 화면에 시각적으로 표시되도록 하면 작업이 매우 수월해집니다.
<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>
<style>
.section { height: 120vh; display: flex; align-items: center; justify-content: center; }
.target { width: 100px; height: 100px; background: #10b981; }
</style>
<div class="section">내려보세요</div>
<div class="section">
<div class="target"></div>
</div>
<script>
gsap.registerPlugin(ScrollTrigger);
gsap.to(".target", {
scrollTrigger: {
trigger: ".target",
// 요소의 'top'이 뷰포트의 'center'에 도달할 때 시작
start: "top center",
// 요소의 'bottom'이 뷰포트의 'top'에 도달할 때 종료
end: "bottom top",
markers: true // 개발용 시각적 마커 표시 (배포 시 제거!)
},
x: 300,
opacity: 0.5,
duration: 1
});
</script>📌 패럴랙스 스크롤의 마법, scrub과 pin
단순히 특정 지점에 도달했을 때 재생되는 것을 넘어, 마우스 휠(스크롤)을 굴리는 속도와 방향에 맞춰 비디오를 탐색하듯 애니메이션이 진행되게 하려면 scrub을 사용합니다.
또한, 애니메이션이 진행되는 동안 화면 스크롤을 특정 섹션에 묶어두고(고정) 싶을 때는 pin: true를 사용합니다. 이 두 가지를 결합하면 매우 역동적인 스토리텔링 웹 페이지를 만들 수 있습니다.
<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>
<style>
.panel { height: 100vh; display: flex; align-items: center; justify-content: center; font-size: 2rem;}
.pin-section { background: #0f172a; color: white; height: 100vh; }
.ghost { font-size: 4rem; display: inline-block; }
</style>
<div class="panel">스크롤 시작</div>
<div class="panel pin-section">
<div class="ghost">👻</div>
</div>
<div class="panel">스크롤 끝</div>
<script>
gsap.registerPlugin(ScrollTrigger);
gsap.to(".ghost", {
x: 300,
rotation: 360,
scrollTrigger: {
trigger: ".pin-section",
start: "top top", // 섹션이 화면 최상단에 닿을 때
end: "+=1000", // 1000px 만큼 스크롤하는 동안 애니메이션 진행
scrub: 1, // 1초 딜레이의 부드러운 스크럽 동기화
pin: true // 애니메이션이 끝날 때까지 화면 고정
}
});
</script>🔄 다시 올라갈 때는 어떻게 할까?
요소가 화면에 나타나면 애니메이션이 재생(Play)되었습니다. 스크롤을 더 내려서 요소가 화면 밖으로 벗어나면 어떻게 될까요? 스크롤을 다시 올려서 요소가 보일 때는요?
toggleActions는 이 4가지 진입/이탈 상태(onEnter, onLeave, onEnterBack, onLeaveBack)에 대해 각각 "play", "pause", "resume", "reverse", "restart", "none" 중 어떤 행동을 취할지 문자열로 제어합니다.
<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>
<style>
.space { height: 100vh; }
.box { width: 100px; height: 100px; background: #eab308; margin: 0 auto; }
</style>
<div class="space"></div>
<div class="box"></div>
<div class="space"></div>
<script>
gsap.registerPlugin(ScrollTrigger);
gsap.to(".box", {
scale: 2,
rotation: 180,
duration: 1,
scrollTrigger: {
trigger: ".box",
start: "top 80%",
end: "bottom 20%",
// 순서: 진입(내릴때) 벗어남(내릴때) 다시진입(올릴때) 다시벗어남(올릴때)
toggleActions: "play pause reverse reset",
markers: true
}
});
</script>🌊 부드러운 휠 스무딩(Smooth Scrolling)
네이티브 휠 스크롤은 뚝뚝 끊기는 느낌이 강합니다. 현대 웹에서는 Lenis 같은 스무스 스크롤 라이브러리를 적용하여 마우스 휠에 관성을 부여합니다.
Lenis 스크롤이 움직일 때마다 GSAP의 ScrollTrigger.update()를 호출하고, GSAP의 ticker에 Lenis 렌더링 루프를 연동해주면 스크롤 바운싱 없이 완벽하게 부드러운 스크럽 애니메이션이 완성됩니다.
<!-- Lenis + GSAP -->
<script src="https://unpkg.com/@studio-freight/lenis@1.0.34/dist/lenis.min.js"></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>
<script>
// 1. Lenis 초기화
const lenis = new Lenis({
duration: 1.2,
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t))
})
// 2. Lenis 스크롤 시 ScrollTrigger 업데이트 동기화
lenis.on('scroll', ScrollTrigger.update)
// 3. GSAP Ticker에 Lenis 연결
gsap.ticker.add((time)=>{
lenis.raf(time * 1000)
})
gsap.ticker.lagSmoothing(0)
// 이후 평소처럼 ScrollTrigger 작성
</script>🔠 글자 하나하나에 생명력을 불어넣기
제목이나 문장의 글자 단위(Char), 단어 단위(Word)로 애니메이션을 주기 위해 텍스트를 DOM 엘리먼트로 쪼개는 기법이 자주 사용됩니다. (GSAP의 SplitText 플러그인 또는 SplitType 같은 무료 라이브러리 활용)
쪼개진 글자 배열을 대상으로 GSAP의 stagger 속성을 적용하면, 마치 타이핑을 치거나 글자들이 날아와서 조립되는 듯한 인상적인 텍스트 모션을 손쉽게 구현할 수 있습니다.
<!-- 텍스트 분할을 위한 SplitType 라이브러리 사용 예시 -->
<script src="https://unpkg.com/split-type"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<h1 class="title" style="font-size: 3rem; clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%);">
Welcome to GSAP World
</h1>
<script>
// 텍스트를 단어와 글자 단위로 <span>으로 분할
const myText = new SplitType('.title', { types: 'chars' })
// 분할된 글자(chars)들을 대상으로 애니메이션
gsap.from(myText.chars, {
y: 100,
opacity: 0,
rotation: 15,
stagger: 0.05,
duration: 0.8,
ease: "back.out(1.5)"
});
</script>⚛️ React와 GSAP의 우아한 만남: useGSAP
React 환경(가상 DOM)에서 GSAP(실제 DOM 조작)을 사용할 때는 컴포넌트가 언마운트될 때 진행 중인 애니메이션과 ScrollTrigger를 정리(Cleanup)해 주어야 메모리 누수와 버그를 막을 수 있습니다.
기존에는 useEffect와 gsap.context()를 묶어서 복잡하게 처리했지만, GSAP 공식 React 훅인 @gsap/react (useGSAP)를 사용하면 내부적으로 스코프 격리와 클린업을 완벽하게 자동 처리해 줍니다.