GSAP 3.15 · INK GREEN

저사양에서도 매끄럽게,
GSAP 애니메이션 20

한 섹션에 한 가지만 담았어요. 화면에서 움직이는 데모와 그 아래 코드는 같은 함수예요 — 코드 칸은 실행 중인 함수를 그대로 찍어 보여 주기 때문에 설명과 동작이 어긋날 수 없어요.

기기 확인 중… 모션 설정 확인 중…

00 · 공통 뼈대

20개 예제가 함께 쓰는 저사양 원칙

예제마다 따로 챙기면 반드시 하나쯤 빠져요. 그래서 화면 밖이면 멈추기 · 모션 줄이기 설정 존중 · 이벤트 정리는 아래 mount()가 한곳에서 맡고, 각 예제는 애니메이션만 만들어 넘겨요.

  • transform · opacity만 움직여요. x y scale rotation autoAlpha — width·top·margin은 매 프레임 레이아웃을 다시 계산해요.
  • 화면 밖이면 멈춰요. 섹션마다 ScrollTrigger 하나가 보일 때만 play(), 벗어나면 pause() 해요. 무한 반복 20개가 동시에 돌지 않아요.
  • prefers-reduced-motion을 존중해요. gsap.matchMedia()로 나눠, 모션 줄이기를 켠 사람에게는 완성된 모습만 보여 줘요.
  • 렉이 걸려도 점프하지 않아요. lagSmoothing(500, 33) — 0.5초 넘게 멈춘 프레임은 33ms만 흐른 것으로 쳐요.
  • 비싼 CSS를 뺐어요. 내비의 backdrop-filter blur, html smooth scroll을 끄고, 무대(.stage)는 contain: layout paint로 가뒀어요.
  • 저사양 기기를 감지해요. 코어 4개 이하이거나 메모리 2GB 이하면 요소 수를 줄여요. ?lite=1로 강제로 켤 수 있어요.
공통 뼈대 · setupGlobal / setupChrome / mount / start

01 · CORE

기본 트윈 — to · from · fromTo

모든 GSAP 애니메이션의 출발점이에요. to()는 현재 → 목표, from()은 목표 → 현재(등장 연출), fromTo()는 시작과 끝을 둘 다 못박아요.

  • xPercent · rotation · scale은 transform이라 레이아웃 재계산이 없어요
  • 이동 거리는 투명 러너의 xPercent: 100 — 화면 크기가 바뀌어도 다시 잴 필요가 없어요
to() · 왼쪽 → 오른쪽
from() · 오른쪽에서 들어옴
fromTo() · 0 → 가운데, 작게 → 크게
코드 · 주석

02 · EASE

이징 비교 — 같은 거리, 다른 성격

거리와 시간이 같아도 이징이 느낌을 만들어요. 기본은 power2.out, 통통 튀는 느낌은 back·elastic, 기계적인 진행은 none이에요.

  • 점 6개가 동시에 움직여도 transform뿐이라 합성 단계에서 끝나요
  • elastic · bounce는 수식 계산이 조금 더 들 뿐, 그리는 비용은 같아요
코드 · 주석

03 · STAGGER

그리드 스태거 — 파동의 출발점 바꾸기

stagger 하나로 수십 개 요소에 시간차를 줘요. grid: 'auto'가 행과 열을 알아서 읽고, from으로 파동이 시작되는 곳을 정해요.

  • 요소마다 트윈을 따로 만들지 않고, 트윈 하나가 전부 처리해요
  • 저사양 기기에서는 칸 수가 자동으로 줄어요 (72 → 40)
  • 칸마다 will-change를 걸지 않아요 — GSAP의 force3D:'auto'가 트윈하는 동안만 레이어로 올려요
from: 'center'
코드 · 주석

04 · TIMELINE

타임라인 — 순서 짜기와 재생 제어

순서가 있는 연출은 delay를 쌓지 말고 타임라인으로 짜요. 위치 인자('<' '-=0.2')와 라벨로 흐름을 만들고, play · pause · reverse · progress로 조종해요.

  • 슬라이더를 움직이면 progress()만 호출해요 — 새 트윈을 만들지 않아요
  • 사용자가 멈추면 스크롤로 다시 들어와도 저절로 재생하지 않아요
시작하기
코드 · 주석

05 · KEYFRAMES

키프레임 — 공 튀기기

여러 단계를 keyframes 배열 하나에 담았어요. 올라갈 땐 느려지고, 내려올 땐 빨라지고, 바닥에 닿는 순간 납작해졌다가(squash) 탄성으로 돌아와요.

  • 찌그러짐은 scaleX · scaleY로 — width · height를 바꾸지 않아요
  • transformOrigin을 공 바닥에 둬서, 위치를 보정하는 계산이 필요 없어요
keyframes: [ 위로 · 아래로 · 납작 · 복원 ]
코드 · 주석

06 · REPEAT · YOYO

반복 · 요요 — 로딩 인디케이터 4종

repeat: -1yoyo로 끝없이 도는 로더를 만들어요. repeat를 stagger 객체 안에 넣으면 점마다 따로 반복해요.

  • 무한 반복은 화면 밖에서 반드시 멈춰야 해요 — 네 개를 타임라인 하나에 담아 한 번에 멈춰요
  • 진행 막대는 width가 아니라 scaleX로 채워요
stagger.repeat
ease: 'none'
scale + autoAlpha
scaleX
코드 · 주석

07 · SPLITTEXT

SplitText — 줄마다 솟아오르는 문장

SplitText가 문장을 줄과 단어로 쪼개고, mask: 'lines'가 줄마다 가림막을 씌워요. 단어는 가림막 아래에서 올라와요.

  • 필요한 단위만 쪼개요(lines, words) — chars까지 쪼개면 요소 수가 몇 배로 늘어요
  • autoSplit은 폰트 로딩이나 폭 변화로 줄바꿈이 달라질 때만 다시 쪼개요
type: 'lines, words' · mask: 'lines'

작은 기기에서도
문장은 부드럽게
떠오른다

코드 · 주석

08 · SCRAMBLETEXT · TEXTPLUGIN

스크램블 · 타이핑

ScrambleText는 무작위 글자를 거쳐 문장을 드러내고, TextPlugin은 한 글자씩 타이핑해요. 섞는 글자로 한글도 쓸 수 있어요.

  • 줄 높이를 고정하고 contain으로 가둬서, 글자가 바뀌어도 그 줄 밖으로 레이아웃이 번지지 않아요
  • 커서 깜빡임은 steps(1) — 중간 투명도 프레임을 그리지 않아요
scrambleText
 
text (TextPlugin)
코드 · 주석

09 · COUNTER

숫자 카운터 — 객체를 트윈하기

DOM이 아니라 평범한 객체의 숫자를 트윈하고, onUpdate에서 글자로 옮겨요. 천 단위 쉼표도, 소수점도 자유롭게 표시할 수 있어요.

  • 표시할 글자가 실제로 바뀐 프레임에만 textContent를 써요
  • tabular-nums로 숫자 폭을 고정해서, 값이 바뀌어도 주변이 흔들리지 않아요
교육 참여
0
만족도
0.0%
절감 시간
0시간
코드 · 주석

10 · DRAWSVG

DrawSVG — 선이 그어지는 차트

DrawSVG는 stroke가 보이는 구간을 트윈해요. 눈금이 깔리고, 추세선이 그어지고, 데이터 점이 올라온 뒤 체크 표시로 마무리돼요.

  • SVG는 작게, 경로는 단순하게 — 다시 그리는 면적이 곧 비용이에요
  • 점 등장은 scale로 — 반지름(r) 속성을 트윈하지 않아요
drawSVG: '0%' → '100%'
코드 · 주석

11 · MORPHSVG

MorphSVG — 모양이 바뀌는 도형

점 개수가 달라도 MorphSVG가 알아서 맞춰 줘요. 원 → 둥근 사각형 → 별 → 하트 → 원으로 이어져요.

  • 회전은 SVG 경로가 아니라 감싼 div의 rotation으로 — 합성 단계에서 처리돼요
  • 복잡한 모양이라 시작이 버벅이면 precompile로 계산을 미리 해 둬요
코드 · 주석

12 · MOTIONPATH

MotionPath — 곡선을 따라 나는 비행기

곡선 경로를 따라 종이비행기가 날고, autoRotate가 기수를 진행 방향으로 돌려요. 지나간 자리에는 DrawSVG로 궤적이 남아요.

  • 비행기를 같은 SVG 안에 둬서 viewBox가 크기를 맞춰 줘요 — 리사이즈해도 다시 계산하지 않아요
  • 궤적과 비행기가 같은 duration · ease를 써서 따로 맞추는 코드가 필요 없어요
motionPath · autoRotate
코드 · 주석

13 · MARQUEE · TIMESCALE

무한 마퀴 — 끝없이 흐르는 띠

로고 띠나 공지 띠처럼 끝없이 흐르는 줄이에요. 같은 묶음을 두 벌 이어 붙이고 xPercent: -50까지 옮긴 뒤 반복하면 이음새 없이 돌아요. 마우스를 올리면 느려지고, 위로 스크롤하면 거꾸로 흘러요.

  • 한 줄에 트윈 하나 — 항목이 몇 개든 움직이는 건 트랙 하나예요
  • 속도·방향은 timeScale만 바꿔요 — 트윈을 새로 만들거나 위치를 다시 재지 않아요
  • 스크롤 방향이 바뀔 때만 반응해요 — 스크롤할 때마다 트윈을 만들지 않아요
timeScale: 1
transformopacityquickTostaggertimelinematchMediapauselagSmoothing
저사양60fps합성 단계레이아웃 0화면 밖 정지모션 줄이기트윈 재사용
코드 · 주석

14 · SCRUB

스크롤 패럴랙스 — scrub

scrub은 애니메이션 진행도를 스크롤 위치에 묶어요. 레이어마다 움직이는 양을 다르게 주면 깊이감이 생겨요. 스크롤하면서 보세요.

  • yPercent만 써요 — background-position 패럴랙스는 매 프레임 다시 칠해요
  • scrub: 0.6처럼 숫자를 주면, 뚝뚝 끊겨 들어오는 스크롤 이벤트 사이를 부드럽게 이어 줘요
  • 모션 줄이기가 켜져 있으면 패럴랙스를 아예 만들지 않아요(어지럼 유발 1순위)
깊이는 속도 차이다
코드 · 주석

15 · PIN · SNAP

고정하고 단계 넘기기 — pin + snap

pin으로 무대를 붙잡아 두고 스크롤한 거리만큼 타임라인이 진행돼요. 스크롤을 멈추면 snap: 'labels'가 가장 가까운 단계로 맞춰 줘요. 고정은 무대 하나만 하고, 움직이는 건 그 안의 카드뿐이에요.

1
STEP 1

기록한다

무엇이 느린지 모르면 고칠 수 없어요. 개발자 도구 Performance 탭에서 저사양 CPU(4× slowdown)로 녹화해요.

STEP 2

원인을 가른다

보라색(Layout)·초록색(Paint)이 길면 속성이 문제고, 노란색(Script)이 길면 트윈 수나 콜백이 문제예요.

STEP 3

transform으로 옮긴다

top·width를 x·scale로, 매번 만드는 트윈을 quickTo로, 화면 밖 애니메이션은 pause로 바꿔요.

코드 · 주석

16 · HORIZONTAL · CONTAINERANIMATION

가로 스크롤 — 세로로 굴려 가로로 밀기

세로 스크롤로 가로 트랙을 밀어요. 트랙을 움직이는 트윈을 containerAnimation에 넘기면, 각 패널이 가로로 들어오는 순간에 따로 반응할 수 있어요. 트랙 이동에는 반드시 ease: 'none'을 써야 스크롤과 위치가 1:1로 맞아요.

PANEL 1
60fps

프레임당 16.7ms. 이 안에 계산·레이아웃·페인트·합성이 다 끝나야 해요.

PANEL 2
합성

transform·opacity는 합성 단계만 거쳐요. GPU가 맡고 메인 스레드는 쉬어요.

PANEL 3
1 트윈

stagger·quickTo·timeline으로 만드는 트윈의 수를 줄여요.

PANEL 4
pause

보이지 않는 애니메이션은 비용만 들어요. 화면 밖이면 멈춰요.

PANEL 5
존중

모션 줄이기를 켠 사람에게는 완성된 화면만 보여 줘요.

코드 · 주석

17 · FLIP

Flip — 레이아웃이 바뀌어도 이어지게

Flip은 바뀌기 전 상태를 기록 → DOM을 바꾸고 → 그 차이를 transform으로 메워요. 그리드 ↔ 리스트 전환이나 필터링이 끊기지 않고 이어져요.

  • 이동은 transform으로 메우고, 전환 중엔 absolute로 빼내서 옆 타일이 밀리는 연쇄 레이아웃이 없어요
  • 상자 높이를 고정해서 전환해도 페이지 높이가 변하지 않아요 — 아래쪽 ScrollTrigger 위치가 흔들리지 않아요
  • 보일 때만 자동 시연하고, 버튼을 누르면 시연을 멈춰요
그린 1
먹 1
그린 2
그린 3
먹 2
그린 4
먹 3
그린 5
코드 · 주석

18 · DRAGGABLE · INERTIA

드래그 캐러셀 — 던지면 미끄러져 멈춘다

Draggable로 끌고 InertiaPlugin으로 던지면, 관성으로 미끄러지다가 snap이 정한 가장 가까운 카드에 멈춰요.

  • 가로로만 끌어요(type: 'x') — 세로 스와이프는 브라우저 기본 스크롤로 넘어가요
  • 카드 치수는 처음과 리사이즈 때만 재고, 표시 점은 인덱스가 바뀔 때만 바꿔요
01끌어 보세요 →
02세게 던지면 멀리
03놓으면 카드에 딱
04끝에서는 저항
05버튼으로도 이동
06마지막 카드
코드 · 주석

19 · QUICKTO

포인터 따라오기 · 자석 버튼

마우스를 따라오는 원과 끌려오는 버튼이에요. 움직일 때마다 트윈을 새로 만들지 않고, gsap.quickTo()로 만든 함수에 값만 넘겨요.

  • pointermove는 1초에 수백 번 와요 — quickTo는 트윈 하나를 계속 재사용해요
  • 위치 측정(getBoundingClientRect)은 들어올 때 한 번만 해요. 스크롤하면 무효로 하고 다음 움직임 때 다시 재요
마우스를 무대 위에서 움직여 보세요
코드 · 주석

20 · OBSERVER

스와이프 카드 — Observer

Observer는 마우스와 터치 입력을 하나로 정리해 줘요. 카드를 좌우로 밀면 넘어가고, 끄는 동안에는 손을 살짝 따라와요.

  • 휠 입력은 받지 않아요 — 페이지 스크롤을 가로채지 않아요
  • 넘어가는 동안에는 입력을 무시해서 트윈이 겹겹이 쌓이지 않아요
CARD 1좌우로
밀어 보세요
CARD 260px 넘게 밀면
넘어가요
CARD 3덜 밀면
제자리로
CARD 4마지막 다음은
처음으로
1 / 4
코드 · 주석

CHECKLIST

저사양에서 매끄럽게 — 내보내기 전 10가지

  1. transform · opacity만 트윈하나요? (x, y, scale, rotation, autoAlpha)
  2. 화면 밖 무한 반복을 멈추나요? (ScrollTrigger onToggle → pause)
  3. pointermove · scroll에서 트윈을 새로 만들고 있지 않나요? (quickTo · quickSetter)
  4. will-change를 움직이는 요소에만 걸었나요?
  5. blur · box-shadow · filter를 트윈하고 있지 않나요?
  6. scrub에 숫자를 줬나요? 가로 스크롤 트랙은 ease: 'none'인가요?
  7. 레이아웃 읽기(offsetWidth, getBoundingClientRect)가 매 프레임 일어나지 않나요?
  8. 콘텐츠 높이가 변하면 ScrollTrigger.refresh()를 부르거나 높이를 고정했나요?
  9. prefers-reduced-motion에서 완성된 화면이 보이나요?
  10. 개발자 도구에서 CPU 4× slowdown으로 한 번 스크롤해 봤나요?