본문으로 건너뛰기
5분 AI.
REPO DEEP DIVE · 2026-07-29 · GREENSOCK/GSAP · 웹 애니메이션의 사실상 표준 엔진

greensock/GSAP 딥다이브
— "모든 것을 움직이는" 단일 틱 엔진, 이제 100% 무료

라이브러리

빠른 판단

무엇
DOM·SVG·Canvas·JS 객체 속성의 보간과 타임라인·스크롤 연동을 제공하는 애니메이션 엔진
이럴 때
여러 요소가 순서대로 움직이는 장면이나 스크롤 진행도에 반응하는 웹 연출을 만들 때
GSAP(GreenSock Animation Platform)는 CSS·SVG·캔버스·WebGL·일반 JS 객체까지 "숫자로 표현되는 모든 값"을 시간에 따라 부드럽게 바꿔주는 프레임워크 무관 애니메이션 엔진이다. React든 Vue든 순수 HTML이든 상관없이 붙는다.

2008년부터 개발된 이 라이브러리는 2025년 Webflow에 인수된 뒤 유료였던 "Club GSAP" 전용 플러그인(ScrollTrigger, SplitText, MorphSVG, Draggable 등)이 전부 무료로 풀리면서 다시 한 번 크게 주목받고 있다. 코드 몇 줄로 스크롤 연동 애니메이션·텍스트 분할·모핑까지 되는데 심지어 공짜라는 사실이 확산의 기폭제가 됐다.

(저장소 greensock/GSAP · 제작 Jack Doyle(GreenSock), 현 Webflow 소속 · 라이선스 Standard "no-charge" license(MIT 아님) · 버전 3.15.0 · 런타임 의존성 0개 · 코어 gsap-core.js 약 3,250줄 + 플러그인 20여 개 · 최신 커밋 13e2b79, 2026-04-13 · 12M+ 사이트에서 사용 중이라고 스스로 소개)
목차
  1. 한 줄 정체
  2. 왜 지금 뜨는가
  3. 무엇을 해결하나
  4. 전체 구조
  5. 핵심 설계·동작 원리
  6. 기술 스택·의존성
  7. 설치·요구사항
  8. 실제 사용 흐름
  9. 함정·오해 방지
  10. 대안 비교
  11. 정리 — 누가 쓰면 좋은가
  12. 직접 해볼 수 있는 실습 과제
  13. 관련 기술 심화 학습 로드맵
  14. 핵심 키워드 사전

1한 줄 정체

"타깃이 무엇이든, 숫자로 표현되는 값이면 시간에 따라 보간해주는 고속 엔진"

GSAP를 한 문장으로 요약하면 "고성능 프로퍼티 조작기(property manipulator)"다. gsap.to(target, {x: 100, duration: 1}) 처럼 "이 대상의 이 속성을 이 값까지, 이 시간 동안 바꿔라"라고 말하면, 매 프레임 그 값을 계산해 대상에 적용해준다. 대상이 DOM 엘리먼트든, SVG든, Canvas 컨텍스트든, {x:0} 같은 순수 JS 객체든 상관하지 않는다.

핵심 저장소인 greensock/GSAP에는 코어 엔진(gsap-core.js)과, 그 위에 얹어 쓰는 20여 개의 플러그인(스크롤 연동 ScrollTrigger, 텍스트 쪼개기 SplitText, 드래그 Draggable, SVG 모핑 MorphSVGPlugin, 레이아웃 전환 Flip 등)이 함께 들어 있다. 예전에는 이 중 다수가 Club GSAP라는 유료 멤버십 전용이었지만, 2025년 Webflow 인수 이후 전부 무료로 전환됐다.

한 줄 비유

"세상 모든 슬라이더 손잡이를 동시에, 정확한 타이밍으로 움직여주는 오케스트라 지휘자"

웹페이지 안의 색상·위치·크기·스크롤 위치는 결국 전부 숫자다. GSAP는 이 숫자 슬라이더들을 "몇 초 동안, 어떤 곡선(easing)으로, 어떤 순서로" 움직일지 지휘하는 지휘자다. 악기(대상)가 바이올린(CSS)이든 드럼(캔버스)이든, 지휘자는 박자(프레임)만 정확히 맞춰주면 된다.

그리고 이 지휘자는 무대(브라우저) 전체에 단 한 명만 존재한다 — 뒤에서 볼 "단일 틱 엔진"이 그 이유다.

2왜 지금 뜨는가

Webflow 인수 + "전부 무료" 선언 · 스크롤 스토리텔링 유행 · React 생태계 편입

GSAP는 신생 라이브러리가 아니다. 오히려 2008년부터 17년 넘게 웹 애니메이션 표준처럼 쓰여 온 "구관"이다. 그런데도 2026년 지금 다시 화제가 되는 이유는 셋으로 요약된다.

① Webflow 인수 → "유료 플러그인 전부 무료" 선언

README에는 이렇게 적혀 있다: "Thanks to Webflow, GSAP is now 100% FREE including ALL of the bonus plugins like SplitText, MorphSVG, and all the others that were exclusively available to Club GSAP members." 예전에는 SplitText·MorphSVGPlugin·InertiaPlugin·ScrollSmoother 같은 강력한 도구를 쓰려면 연 구독료를 내는 Club GSAP 멤버십이 필요했다. 이 장벽이 통째로 사라졌다는 것이 이번 화제의 진앙이다.

② "스크롤이 곧 스토리텔링"이 되는 웹 디자인 트렌드

최근 랜딩페이지·포트폴리오·에이전시 사이트들은 스크롤에 맞춰 요소가 나타나고, 고정(pin)되고, 모양이 바뀌는 연출을 즐겨 쓴다. 이런 연출의 사실상 표준 도구가 바로 무료로 풀린 ScrollTrigger다. 유료 장벽이 없어지자 튜토리얼·템플릿·쇼케이스가 폭발적으로 늘었다.

③ React 생태계와의 자연스러운 결합

공식 보조 패키지 @gsap/react가 제공하는 useGSAP() 훅은 useEffect를 대체하며 정리(cleanup)를 자동화한다. "React는 선언적, GSAP는 명령적"이라는 이질감이 이 훅과 gsap.context() 덕분에 크게 줄어, React/Next.js 프로젝트에서도 부담 없이 쓰이게 됐다.

경쟁 지형
Motion(구 Framer Motion)·anime.js·CSS 애니메이션 사이에서

선언적 React 애니메이션은 Motion이, 가벼운 범용 트위닝은 anime.js가 맡는 영역이 있다. GSAP의 차별점은 타임라인 기반 정밀 시퀀싱 + 스크롤/드래그/모핑까지 아우르는 플러그인 생태계다. "간단한 트랜지션 하나"가 아니라 "정교하게 지휘된 다단계 연출"이 필요할 때 GSAP가 선택된다. 자세한 비교는 10번 섹션 참고.

3무엇을 해결하나

브라우저 애니메이션의 3대 고질병 — 크로스브라우저 불일치 · 시퀀싱 난이도 · 성능

순수 CSS transition/@keyframes나 Web Animations API만으로도 애니메이션은 가능하다. 그런데도 GSAP가 필요한 이유는 CSS·JS 애니메이션이 실제 제품에서 부딪히는 세 가지 벽 때문이다.

① 크로스브라우저 변환(transform) 불일치

transform: translateX()와 rotate()를 동시에 쓰거나, SVG 요소에 CSS 변환을 걸 때 브라우저마다 미묘하게 다르게 동작하던 시절이 있었다. GSAP의 CSSPlugin은 이런 불일치를 흡수해 "당신의 애니메이션은 그냥 동작한다(just works)"는 약속을 지킨다.

② 복잡한 시퀀싱을 코드로 표현하기 어려움

"A가 끝나고 0.2초 뒤 B와 C가 동시에 시작하고, 그 중간 지점에 D가 끼어든다" 같은 시나리오를 setTimeout이나 CSS 애니메이션 지연 값 계산으로 짜면 순식간에 스파게티가 된다. GSAP의 타임라인(Timeline)은 이런 시퀀스를 "타임코드"라는 직관적인 위치 파라미터("-=0.2", "<" 등)로 선언적으로 표현하게 해준다.

③ 대량 애니메이션의 성능

한 페이지에 수백 개의 트윈이 동시에 돌아가면, 트윈마다 개별 requestAnimationFrame 콜백을 등록하는 순진한 구현은 금방 버벅인다. GSAP는 페이지 전체의 모든 애니메이션을 단 하나의 rAF 루프로 묶어 처리한다(5번 섹션에서 자세히). README는 "jQuery보다 최대 20배 빠르다"고 주장하는데, 근거가 바로 이 단일 틱 구조와 프로퍼티별 최적화된 렌더러다.

용어
트위닝 (tweening)
"in-betweening"의 줄임말. 시작 값과 끝 값 사이의 중간(between) 프레임들을 자동으로 계산해 채워주는 것. GSAP의 gsap.to()가 하는 일이 바로 이것이다.
용어
타임라인 (Timeline)
여러 개의 트윈(또는 다른 타임라인)을 순서·겹침·간격까지 지정해 하나의 시퀀스로 묶는 컨테이너. 영상 편집 소프트웨어의 타임라인과 개념이 같다.

4전체 구조

코어 1개 + 플러그인 20여 개, "필요한 것만 골라 등록"하는 구조

src/ 폴더를 열어보면 구조가 명확하다. 코어(gsap-core.js, 약 3,255줄) 하나가 엔진·타임라인·트윈·플러그인 등록 시스템을 전부 담당하고, 나머지는 전부 독립된 플러그인 파일이다. 코어만 있어도 순수 JS 객체 애니메이션은 되지만, DOM을 움직이려면 CSSPlugin부터 필요하다.

┌─────────────────────────────────────────────────────────────────┐ │ GSAP 저장소 구조 (src/) │ └─────────────────────────────────────────────────────────────────┘ gsap-core.js # ★ 심장 — Animation/Tween/Timeline, 틱 엔진, 플러그인 레지스트리 index.js # npm "gsap" 메인 진입점 — 코어 + CSSPlugin 만 (얇음!) all.js # "gsap/all" — 사실상 모든 플러그인을 한 번에 export ── DOM/시각 기본 ── CSSPlugin.js CSSRulePlugin.js EasePack.js ── 스크롤 계열 ── ScrollTrigger.js (1,790줄) ScrollSmoother.js Observer.js ── 인터랙션 ── Draggable.js (1,950줄) InertiaPlugin.js Physics2DPlugin.js PhysicsPropsPlugin.js ── SVG/모핑/텍스트 ── MorphSVGPlugin.js (859줄) DrawSVGPlugin.js MotionPathPlugin.js MotionPathHelper.js SplitText.js/.ts (333/517줄) TextPlugin.js ScrambleTextPlugin.js ── 레이아웃 전환 ── Flip.js (1,058줄) ── 이징 ── CustomEase.js CustomBounce.js CustomWiggle.js ── 렌더러 통합 ── PixiPlugin.js EaselPlugin.js ── 개발 도구 ── GSDevTools.js ── 빌드 산출물 ── dist/ # UMD(전역 <script>/CommonJS), *.min.js + .map esm/ # ES 모듈 (번들러용) types/ # TypeScript .d.ts

구조가 주는 메시지는 명확하다. "코어는 아무것도 모른다". 코어는 CSS도, 스크롤도, SVG도 모른다. 오직 "속성을 시간에 따라 보간하고, 그 결과를 렌더러에게 넘긴다"는 일반적인 메커니즘만 안다. DOM을 움직이는 지식(transform 파싱, 단위 처리 등)은 전부 CSSPlugin에, 스크롤 위치 감지는 ScrollTrigger에 위임돼 있다. 이 관심사 분리 덕분에 필요한 기능만 골라 번들 크기를 최소화할 수 있다.

구조상 주의
"gsap" 메인 패키지 ≠ "모든 기능 포함"

package.json의 main/module이 가리키는 index.js는 코어 + CSSPlugin뿐이다. ScrollTrigger·Draggable·SplitText 등은 전부 import ScrollTrigger from "gsap/ScrollTrigger" 식으로 따로 import하고 registerPlugin해야 쓸 수 있다. "gsap 하나만 설치하면 다 된다"는 오해가 흔하다.

5핵심 설계·동작 원리

단일 틱 엔진 · Animation→Tween/Timeline 계층 · PropTween 렌더 파이프라인 · 플러그인 자가등록 · Context/matchMedia

이 섹션이 이 문서에서 가장 중요하다. GSAP가 왜 빠르고, 왜 "타임라인"이라는 개념이 강력한지는 전부 gsap-core.js 소스 안에 있다.

① 단일 틱 엔진 — 페이지 전체에 rAF 콜백은 단 하나

gsap-core.js 안의 _ticker는 즉시실행함수로 만들어진 독립 객체다. 실제 코드를 보면:

_ticker = (function() {
  let _getTime = Date.now,
    _gap = 1000 / 240,           // 내부 정밀도 240fps 기준
    _tick = v => {
      let elapsed = _getTime() - _lastUpdate, ...
      if (overlap > 0 || manual) {
        frame = ++_self.frame;
        _self.time = time = time / 1000;
        dispatch = 1;
      }
      manual || (_id = _req(_tick));      // 다음 rAF 예약
      if (dispatch) {
        for (_i = 0; _i < _listeners.length; _i++) {
          _listeners[_i](time, _delta, frame, v);  // 리스너 전원 호출
        }
      }
    };
  ...
})();

// 코어 파일 맨 끝부분:
_ticker.add(Timeline.updateRoot);   // 전체 애니메이션 트리의 유일한 진입점

즉 브라우저의 requestAnimationFrame은 딱 하나만 걸려 있고, 그 콜백이 _listeners 배열을 순회한다. 그런데 이 배열에 등록된 리스너는 사실상 Timeline.updateRoot 단 하나뿐이다 — 이것이 루트 타임라인(_globalTimeline)을 갱신하고, 루트가 자신의 모든 자식(하위 타임라인·트윈)을 재귀적으로 갱신한다. 페이지에 트윈이 1개든 1만 개든 브라우저 입장에서는 rAF 콜백이 딱 하나인 셈이다.

비유

학교에서 "종을 치는 사람"이 한 명뿐이라고 생각해보자. 종이 울리면(rAF 틱) 교장선생님(루트 타임라인)이 그 신호를 받아 각 학년 부장(하위 타임라인)에게 전달하고, 부장은 다시 각 반 담임(개별 트윈)에게 전달한다. 학생(속성값) 수가 아무리 많아도 종은 하나다 — 이게 GSAP가 대량의 애니메이션에서도 버벅이지 않는 이유다.

② Animation → Tween / Timeline 계층 구조

gsap-core.js에는 class Animation(공통 기반), 그 아래 class Tween extends Animation과 class Timeline extends Animation이 정의돼 있다. Timeline은 자식으로 Tween이나 다른 Timeline을 가질 수 있는 합성(Composite) 구조다. 실제로 페이지의 전체 애니메이션은 _globalTimeline = new Timeline({id:"root", ...})라는 단 하나의 루트를 정점으로 한 트리(연결 리스트, _first/_next로 순회)를 이룬다. gsap.timeline()으로 만든 타임라인도 결국 이 루트의 자식으로 편입된다.

클래스역할
Animation재생/일시정지/시간 탐색(seek)/진행률(progress) 등 모든 애니메이션 공통 기능의 기반 클래스
Tween실제 속성값을 바꾸는 최소 단위. _ptLookup(대상별 PropTween 목록)을 들고 있음
Timeline여러 Tween/Timeline을 순서·겹침으로 배치하는 컨테이너. 자신도 Animation이라 타임라인끼리 중첩 가능

③ PropTween — 속성 하나하나가 연결 리스트 노드가 된다

트윈이 실제로 값을 바꾸는 최소 단위는 class PropTween이다. 대상(target)·속성명(p)·렌더 함수(r)·시작값(s)·변화량(c)·setter를 들고 있는 가벼운 객체이며, 트윈 하나가 여러 속성(x, y, opacity...)을 가지면 그만큼의 PropTween이 연결 리스트(_pt → _next)로 이어진다. 매 틱마다 이 리스트를 한 번 순회하며 각 노드의 렌더 함수를 호출하는 것만으로 모든 속성이 갱신된다 — 배열 생성이나 객체 순회 오버헤드를 최소화한 설계다.

용어
PropTween
"Property Tween"의 줄임. 하나의 속성(예: x좌표)에 대한 시작값·끝값·렌더 방법을 담은 최소 단위 노드. GSAP 내부 성능의 비밀은 이 가벼운 구조체를 연결 리스트로 이어 순회 비용을 최소화한 데 있다.

④ 플러그인 시스템 — 등록(register) 기반의 확장

코어에는 let _plugins = {}라는 빈 레지스트리가 있다. gsap.registerPlugin(ScrollTrigger, Flip, ...)을 호출하면 내부 _createPlugin()이 각 플러그인 설정을 받아 init/render/kill 생명주기를 가진 Plugin 클래스를 만들고 _plugins에 등록한다. 이후 gsap.to(el, {scrollTrigger: {...}})처럼 등록된 이름의 속성을 쓰면 해당 플러그인의 렌더 로직이 PropTween 파이프라인에 끼워 넣어진다.

재미있는 점은 각 플러그인 파일이 스스로 등록을 시도한다는 것이다. 예를 들어 ScrollTrigger.js 맨 끝 줄은 이렇다:

// ScrollTrigger.js 맨 끝
_getGSAP() && gsap.registerPlugin(ScrollTrigger);

// _getGSAP()의 정의:
_getGSAP = () => gsap || (_windowExists() && (gsap = window.gsap) && gsap.registerPlugin && gsap)

즉 <script> 태그로 전역 window.gsap가 이미 있는 환경(브라우저 전역 빌드)에서는 import만 해도 자동으로 등록된다. 하지만 번들러(webpack/Vite) + ESM 환경에서는 전역 window.gsap가 없을 수 있어 자동 등록이 실패하고, 개발자가 직접 gsap.registerPlugin(ScrollTrigger)를 호출해야 한다(9번 섹션의 대표적 함정).

⑤ gsap.context() / matchMedia — 프레임워크 친화적 정리(cleanup)

class Context는 특정 함수 스코프 안에서 생성된 모든 애니메이션(과 셀렉터)을 this.data 배열에 수집한다. context.revert()를 호출하면 수집된 모든 트윈·타임라인을 역순으로 되돌리고 제거한다 — React의 useEffect cleanup이나 컴포넌트 unmount에서 "이 컴포넌트가 만든 애니메이션만 깔끔히 지우기"에 정확히 맞는 도구다. class MatchMedia는 이 Context를 브레이크포인트별로 감싸, 네이티브 window.matchMedia()의 change 이벤트에 맞춰 조건에 맞는 애니메이션만 자동으로 되돌리고 다시 만든다.

실전 연결고리

공식 React 훅 useGSAP()(별도 패키지 @gsap/react)는 내부적으로 바로 이 gsap.context()를 감싼 것이다. "React에서 GSAP 쓰다가 메모리 누수 나는" 문제 대부분은 이 context를 안 쓰고 직접 gsap.to()를 호출해서 생긴다.

⑥ 렌더 파이프라인과 자동 절전(autoSleep)

틱마다 루트 타임라인은 render(totalTime)을 호출해 현재 로컬 시간을 계산하고, 각 자식의 PropTween 리스트를 순회해 값을 적용한다. 흥미로운 디테일은 자동 절전(autoSleep)이다. 코드 상에는 이렇게 돼 있다:

if (_ticker.frame >= _nextGCFrame) {
  _nextGCFrame += _config.autoSleep || 120;
  ...
  child || _ticker.sleep();   // 돌아가는 애니메이션이 없으면 rAF 루프 자체를 정지
}

애니메이션이 하나도 없는 순간에는 _ticker.sleep()으로 rAF 요청 자체를 멈춰 배터리·CPU를 아낀다. 새 트윈이 만들어지면 _wake()가 다시 깨운다.

⑦ CustomEase — SVG 패스를 이징 함수로 압축

CustomEase.js는 SVG cubic-bezier 패스 문자열을 파싱해 내부 좌표 배열로 정규화(_normalize)한 뒤, gsap.registerEase()로 이름 붙은 커스텀 이징 함수를 등록한다. 디자이너가 그래픽 에디터에서 그린 곡선을 그대로 이징으로 쓸 수 있게 해주는 다리 역할이다.

6기술 스택·의존성

런타임 의존성 0개 · UMD/ESM/TypeScript 삼중 빌드 · 버전의 진실원은 package.json
항목내용
런타임 의존성0개. package.json에 dependencies 필드 자체가 없다. README도 "Zero dependencies"를 명시적으로 강조
빌드 산출물dist/(UMD — 전역 <script>·CommonJS require 겸용) · esm/(ES 모듈, 번들러용) · types/(TypeScript .d.ts)
모듈 진입점package.json의 exports 맵: import → esm/index.js, require → dist/gsap.js, types → types/index.d.ts. 하위 경로도 gsap/ScrollTrigger 식으로 동일 매핑
버전 진실원package.json의 "version": "3.15.0". 각 플러그인 소스 파일 상단 주석(/*! ScrollTrigger 3.15.0 */)에도 버전이 중복 기재돼 있어, 배포 시 사람이 함께 맞춰야 하는 구조(수동 동기화 지점)
개발 도구 체인package.json에 scripts·devDependencies 필드가 없다 — 빌드/압축(min)·소스맵 생성 파이프라인은 이 공개 저장소 밖에서 비공개로 돈다. 즉 이 레포는 "소스 + 완성된 빌드 산출물"의 읽기용 미러에 가깝다
테스트/CI레포 안에 .github/workflows·테스트 파일이 보이지 않는다. GreenSock 내부 QA는 비공개로 진행되는 것으로 보이며, 공개 레포만 보고 "테스트가 없다"고 단정하면 오해다
보조 패키지@gsap/react(별도 npm 패키지, 이 레포에는 없음) — useGSAP() 훅으로 React cleanup 자동화
타입스크립트types/에 전체 .d.ts 제공. 다만 SplitText는 src/에 .js(333줄)와 .ts(517줄) 두 버전이 공존 — TS로 전면 재작성 중인 과도기 흔적
용어
UMD (Universal Module Definition)
CommonJS(require)·AMD·전역 <script> 세 가지 모듈 방식을 하나의 파일이 전부 지원하도록 감싸는 패턴. GSAP의 dist/gsap.js 최상단이 이 패턴으로 시작한다.
용어
라이선스 "Standard no-charge license"
MIT·Apache처럼 OSI가 인증한 표준 오픈소스 라이선스가 아니다. GreenSock(Webflow)이 만든 자체 약관으로, "돈을 받지 않는다"는 의미의 무료이지 소스 재배포·재판매 등에는 별도 제약이 있을 수 있는 독점(proprietary) 라이선스다. 실제 GitHub 저장소에는 LICENSE 파일조차 없고, package.json과 README가 gsap.com/standard-license 링크만 가리킨다.

7설치·요구사항

CDN 한 줄 또는 npm install gsap · 브라우저 전용, Node 서버 실행형 아님

GSAP는 서버가 아니라 브라우저(또는 브라우저와 유사한 렌더링 컨텍스트)에서 도는 클라이언트 라이브러리다. "설치"는 사실상 스크립트 참조 또는 npm 패키지 추가가 전부다.

방법 1 — CDN (가장 빠른 시작)

<script src="https://cdn.jsdelivr.net/npm/gsap@3.15/dist/gsap.min.js"></script>
<script>
  gsap.to(".box", { x: 300, duration: 1 });
</script>

전역 빌드이므로 플러그인 스크립트도 순서대로 추가하면 _getGSAP()가 window.gsap을 찾아 자동 등록해준다.

방법 2 — npm (번들러 환경)

npm install gsap
import gsap from "gsap";
import ScrollTrigger from "gsap/ScrollTrigger";
import Flip from "gsap/Flip";

gsap.registerPlugin(ScrollTrigger, Flip);  // 번들러 환경에선 자동등록이 안 되므로 명시 필수
항목요구사항 / 참고
대상 환경모던 에버그린 브라우저(Chrome/Firefox/Safari/Edge). v3부터 IE11 등 구형 지원 부담이 크게 줄었음
번들러ESM(esm/) 인식 가능한 아무 번들러(Webpack/Vite/Rollup/esbuild)나 사용 가능. 트리쉐이킹을 위해 sideEffects:false가 package.json에 명시돼 있음
과거 유료 플러그인 설치이전엔 GreenSock 전용 npm 레지스트리 로그인(.npmrc 토큰)이 필요했지만, 2025년 무료 전환 이후로는 공개 npm에서 바로 설치 가능
SSR (Next.js 등)코어는 _windowExists() 체크로 서버에서 안전하지만, 스크롤·드래그처럼 DOM에 의존하는 플러그인 등록·실행은 클라이언트 전용 컴포넌트/훅 안에서 해야 함
패키지 규모런타임 의존성 0개이므로 node_modules 설치 시 부수 패키지가 딸려오지 않음(gsap 자체만 추가됨)
주의
"npm install gsap"만으로 모든 플러그인이 자동 등록되진 않는다

패키지 설치는 모든 플러그인 파일을 node_modules에 내려받지만, 실제로 쓰려는 플러그인은 개별 import + registerPlugin이 필요하다. "gsap/all"에서 한 번에 가져올 수도 있지만 그만큼 번들 크기가 커진다.

8실제 사용 흐름

타임라인 작성 → 플러그인 등록 → 스크롤 연동 → React에서 안전하게 정리

기본적인 사용 흐름을 단계별로 살펴본다.

1단계 — 단순 트윈과 타임라인

import gsap from "gsap";

// 단발성 트윈: .box를 1초간 x=300으로
gsap.to(".box", { x: 300, duration: 1, ease: "power2.out" });

// 타임라인: 순서·겹침을 타임코드로 지정
const tl = gsap.timeline();
tl.to(".title", { opacity: 1, y: 0, duration: 0.6 })
  .to(".subtitle", { opacity: 1, duration: 0.4 }, "-=0.2")  // 앞 트윈 끝나기 0.2초 전 시작
  .to(".cta", { scale: 1 }, "<");                            // 바로 앞 트윈과 동시 시작

2단계 — ScrollTrigger로 스크롤 연동

import ScrollTrigger from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);

gsap.to(".panel", {
  x: -500,
  scrollTrigger: {
    trigger: ".panel",
    start: "top top",
    end: "+=1000",
    scrub: true,   // 스크롤 위치에 애니메이션 진행률을 직접 연결
    pin: true      // 구간 동안 요소를 화면에 고정
  }
});

3단계 — React에서 gsap.context()로 안전하게 정리

import { useGSAP } from "@gsap/react";
import gsap from "gsap";

function Banner() {
  const container = useRef(null);

  useGSAP(() => {
    gsap.to(".hero", { opacity: 1, y: 0, duration: 0.8 });
    // 컴포넌트가 unmount되면 이 스코프 안 애니메이션이 자동으로 revert됨
  }, { scope: container });

  return <div ref={container}><div className="hero">Hello</div></div>;
}

4단계 — Flip으로 레이아웃 전환 애니메이션

import Flip from "gsap/Flip";
gsap.registerPlugin(Flip);

const state = Flip.getState(".card");  // 변경 "전" 위치·크기 스냅샷
container.appendChild(card);            // DOM 구조를 실제로 바꿈
Flip.from(state, { duration: 0.5, ease: "power1.inOut" }); // "전"에서 "후"로 자연스럽게 보간

이 네 단계는 실제로 GSAP를 쓰는 프로젝트에서 가장 자주 반복되는 패턴이다. 공통점은 "값을 직접 계산해 매 프레임 대입하는" 저수준 작업을 GSAP가 대신 해준다는 것이다.

9함정·오해 방지

"무료 = 오픈소스"는 아니다 · registerPlugin 누락 · README와 실제 코드의 괴리
오해 ①
"무료로 풀렸으니 MIT 오픈소스겠지"

아니다. package.json의 license 필드는 "Standard 'no charge' license"라고 명시돼 있고, 저장소에는 LICENSE 파일 자체가 없다. "돈을 받지 않는다"는 것과 "OSI 기준 오픈소스"는 다른 개념이다. 상업적 재배포·경쟁 제품화 등 세부 조건은 반드시 공식 라이선스 페이지에서 직접 확인해야 한다.

오해 ②
"import만 하면 플러그인이 알아서 등록된다"

전역 <script> 빌드에서는 각 플러그인 파일 끝의 _getGSAP() && gsap.registerPlugin(X) 코드가 window.gsap를 찾아 자동 등록해준다. 하지만 ESM/번들러 환경에서는 이 자동 등록 조건이 성립하지 않는 경우가 많아, gsap.registerPlugin(ScrollTrigger)를 빠뜨리면 "Invalid property... Missing plugin?" 경고만 뜨고 애니메이션이 조용히 무시된다.

오해 ③
README의 "gsap/all excludes members-only plugins"는 낡은 문구

README는 "all tools are exported from the 'all' file (excluding members-only plugins)"라고 쓰여 있지만, 실제 src/all.js를 열어보면 MorphSVGPlugin·SplitText·InertiaPlugin·ScrollSmoother·CustomBounce·GSDevTools 등 예전 Club 전용 플러그인이 이미 다 포함돼 있다. 무료화 이전에 쓰인 문구가 그대로 남아 있는 문서-코드 불일치 사례다. 반대로 gsap 메인 진입점(index.js)은 코어+CSSPlugin뿐인 매우 얇은 파일이라는 점도 헷갈리지 않아야 한다.

오해 ④
"레포에 테스트/CI가 없으니 검증이 부실하다"

공개 GitHub 레포에 .github/workflows나 테스트 파일이 보이지 않는 것은 사실이지만, 이는 GreenSock의 빌드·QA 파이프라인이 비공개로 운영된다는 뜻이지 "검증 안 된 코드"라는 뜻이 아니다. 이 레포는 소스 + 완성된 dist/esm/types 산출물을 함께 배포하는 형태에 가깝다.

오해 ⑤
React에서 gsap.context() 없이 쓰다 생기는 메모리 누수

useEffect 안에서 gsap.to()를 직접 호출하고 정리(cleanup)를 빼먹으면, 컴포넌트가 unmount돼도 트윈이 루트 타임라인에 계속 남아 존재하지 않는 DOM을 참조하려 시도한다. gsap.context() 또는 useGSAP()로 스코프를 감싸고 revert()하는 습관이 필수다.

참고
단일 틱 엔진의 이면 — 무거운 콜백 하나가 전체를 버벅이게 할 수 있다

모든 애니메이션이 하나의 rAF 콜백을 공유한다는 것은 장점이자 위험이다. onUpdate 콜백 안에서 무거운 동기 연산(예: 레이아웃을 강제로 읽는 getBoundingClientRect 남발)을 하면, 그 프레임에서 페이지의 모든 GSAP 애니메이션이 함께 지연된다. 무거운 로직은 쓰로틀링하거나 별도 최적화가 필요하다.

10대안 비교

Motion · anime.js · 순수 CSS/Web Animations API와 무엇이 다른가
도구강점GSAP과의 차이
Motion
(구 Framer Motion)
React에 최적화된 선언적 API, 레이아웃 애니메이션(layout prop) 자동화React 바깥에서는 활용도가 낮음. GSAP는 프레임워크 무관하며 타임라인 기반 정밀 시퀀싱·스크롤/드래그/모핑 플러그인 생태계가 훨씬 넓음
anime.js가볍고(용량 작음) API가 간결. 범용 트위닝에 충분ScrollTrigger급 스크롤 연동, SplitText급 텍스트 분할, MorphSVG급 모핑 같은 전문 플러그인 생태계가 없음. "간단한 트윈 몇 개"엔 충분하지만 복합 연출엔 부족
CSS @keyframes/transition추가 JS 없이 브라우저 네이티브, 성능 좋음런타임 제어(일시정지·되감기·동적 값 계산)가 어렵고, 복잡한 시퀀싱을 코드로 표현하기 힘듦. GSAP는 이걸 JS API로 완전히 통제 가능하게 함
Web Animations API (WAAPI)브라우저 네이티브 표준, 별도 라이브러리 불필요API가 저수준이라 타임라인 시퀀싱·이징 커스터마이징·플러그인 확장성 면에서 GSAP보다 개발자 경험이 떨어짐
LottieAfter Effects로 만든 벡터 애니메이션을 그대로 재생"디자이너가 만든 고정 애니메이션 재생"에 특화. GSAP처럼 코드로 동적·인터랙티브하게 값을 바꾸는 용도는 아님
선택 기준 요약

· React 컴포넌트 자체의 mount/unmount/레이아웃 애니메이션이 중심이면 Motion이 더 자연스러울 수 있다.
· 가볍고 단순한 트윈 몇 개면 anime.js나 CSS로 충분하다.
· 스크롤 스토리텔링, 정밀한 다단계 시퀀싱, SVG 모핑, 드래그, 프레임워크 무관 재사용이 필요하면 GSAP가 사실상 표준이며, 이제 비용 장벽도 없다.

11정리 — 누가 쓰면 좋은가

"타임라인이 필요한 순간" 쓰는 도구 — 마케팅 사이트부터 인터랙티브 대시보드까지

GSAP는 "움직임을 정밀하게 지휘해야 하는 모든 웹 프로젝트"에 잘 맞는다. 특히 다음과 같은 사람·상황에 추천한다.

반대로 버튼 hover 색상 전환처럼 아주 단순한 트랜지션 하나만 필요하다면 굳이 GSAP를 끌어올 필요는 없다 — CSS만으로 충분하고, 번들 크기도 아낄 수 있다. "얼마나 복잡한 타임라인을 지휘해야 하는가"가 GSAP 도입 여부를 가르는 가장 실용적인 기준이다.

12직접 해볼 수 있는 실습 과제

객체 보간 → 타임라인 → 이징 확장 → 컨텍스트 정리, 네 단계로 코어를 확인한다

준비물은 Node.js와 이미 내려받은 GSAP 소스다. 아래 명령은 모두 package.json과 dist/가 있는 GSAP 클론 루트에서 실행한다. 일반 객체의 값을 관찰하므로 화면 구성이나 추가 패키지 설치가 필요 없다.

실습 준비

node --version으로 실행 환경을 확인한다. 이 공개 레포의 package.json에는 빌드·테스트 스크립트가 없고, 예제 폴더도 없다. 실행에는 완성된 dist/ 파일을 쓰고, 동작 원리는 src/와 types/에서 찾는다. 각 명령 끝의 gsap.ticker.sleep()은 이 독립 실습의 공용 타이머를 정리한다.

과제 1 · 입문

시간의 절반과 이동 거리의 절반 확인하기

목표: 트윈이 값을 보간하는 방식과 progress()의 의미를 이해한다.

  1. types/animation.d.ts의 progress() 선언과 src/gsap-core.js의 _renderPlain을 읽는다.
  2. 아래 명령으로 정지된 트윈의 진행률을 직접 옮기고, 각 지점의 값을 검증한다.
node -e '
const assert = require("node:assert/strict");
const { gsap } = require("./dist/gsap.js");
const point = { x: 0 };
const tween = gsap.to(point, { x: 100, duration: 2, ease: "none", paused: true });
const rows = [0, 0.25, 0.5, 1].map(progress => {
  tween.progress(progress);
  assert.equal(point.x, progress * 100);
  return { progress, x: point.x };
});
console.table(rows);
tween.kill();
gsap.ticker.sleep();
'

완료 기준: 표의 x가 차례로 0, 25, 50, 100이고 검증 오류 없이 끝난다. duration: 2에서 progress(0.5)가 1초 지점이라는 것을 설명할 수 있다.

과제 2 · 초급

두 트윈을 겹쳐 배치하고 같은 시각을 탐색하기

목표: 라벨과 상대 위치로 실행 순서를 정하고, 타임라인의 시간과 개별 트윈의 시간을 구분한다.

  1. src/gsap-core.js의 _parsePosition과 Timeline.addLabel(), types/timeline.d.ts의 to()를 읽는다.
  2. 첫 트윈은 0초, 두 번째는 0.5초에 시작하도록 배치한다. 실행 전에 0.75초 지점의 두 값을 손으로 계산한다.
  3. 아래 명령으로 계산을 확인하고, 라벨을 사용해 처음으로 되돌린다.
node -e '
const assert = require("node:assert/strict");
const { gsap } = require("./dist/gsap.js");
const a = { x: 0 };
const b = { x: 0 };
const tl = gsap.timeline({ paused: true, defaults: { duration: 1, ease: "none" } });
tl.addLabel("start", 0)
  .to(a, { x: 100 }, "start")
  .to(b, { x: 100 }, "start+=0.5");
assert.equal(tl.duration(), 1.5);
tl.seek(0.75);
assert.equal(a.x, 75);
assert.equal(b.x, 25);
console.log(tl.duration(), a.x, b.x);
tl.seek("start");
assert.equal(a.x, 0);
assert.equal(b.x, 0);
tl.kill();
gsap.ticker.sleep();
'

완료 기준: 1.5 75 25가 출력되고 처음으로 이동한 뒤 두 값이 모두 0이다. 두 트윈의 길이가 각각 1초인데 전체 길이는 왜 1.5초인지 설명할 수 있다.

과제 3 · 중급

직접 등록한 이징으로 같은 진행률의 값 바꾸기

목표: 시간 진행률이 이징 함수를 거쳐 속성값으로 바뀌는 과정을 확인한다.

  1. src/CustomEase.js의 register()와 create(), 경로 파싱을 맡는 src/utils/paths.js를 읽는다.
  2. 로컬 dist/CustomEase.js를 불러와 명시적으로 등록하고, 두 제어점의 좌표 값 네 개로 곡선을 만든다.
  3. 선형 이징과 사용자 이징의 표를 비교한 뒤, 등록한 이름을 실제 트윈에 적용한다.
node -e '
const assert = require("node:assert/strict");
const { gsap } = require("./dist/gsap.js");
const { CustomEase } = require("./dist/CustomEase.js");
gsap.registerPlugin(CustomEase);
const curve = CustomEase.create("study", "0.25,0.1,0.25,1");
assert.ok(Math.abs(curve(0)) < 0.001);
assert.ok(Math.abs(curve(1) - 1) < 0.001);
assert.ok(curve(0.5) > 0.5);
const target = { x: 0 };
const tween = gsap.to(target, { x: 100, duration: 1, ease: "study", paused: true });
tween.progress(0.5);
assert.ok(target.x > 50);
console.table([0, 0.25, 0.5, 0.75, 1].map(p => ({
  progress: p,
  linear: gsap.parseEase("none")(p).toFixed(3),
  custom: curve(p).toFixed(3)
})));
console.log(target.x);
tween.kill();
gsap.ticker.sleep();
'

완료 기준: 곡선의 시작과 끝이 오차 범위 안에서 0과 1이다. 진행률 0.5에서 선형 값은 0.500, 사용자 곡선 값은 약 0.802이고, 트윈의 x도 50보다 크다.

과제 4 · 심화

한 컨텍스트만 복원하고 바깥 트윈은 유지하기

목표: 애니메이션의 소유 범위를 나누고, 초기값 복원과 단순 중단의 차이를 확인한다.

  1. src/gsap-core.js의 Context.getTweens(), kill(), revert()를 읽는다.
  2. 컨텍스트 안팎에 트윈을 하나씩 만들고 둘 다 중간 지점으로 옮긴다.
  3. 안쪽 컨텍스트를 복원한 뒤, 바깥 트윈은 끝까지 이동할 수 있는지 검증한다.
node -e '
const assert = require("node:assert/strict");
const { gsap } = require("./dist/gsap.js");
const owned = { x: 10 };
const outside = { x: 0 };
const independent = gsap.to(outside, { x: 60, duration: 1, ease: "none", paused: true });
let tween;
const ctx = gsap.context(() => {
  tween = gsap.to(owned, { x: 110, duration: 1, ease: "none", paused: true });
});
tween.progress(0.5);
independent.progress(0.5);
assert.equal(owned.x, 60);
assert.equal(ctx.getTweens().length, 1);
ctx.revert();
assert.equal(owned.x, 10);
assert.equal(outside.x, 30);
assert.equal(ctx.getTweens().length, 0);
independent.progress(1);
assert.equal(outside.x, 60);
console.log(owned.x, outside.x, ctx.getTweens().length);
independent.kill();
gsap.ticker.sleep();
'

완료 기준: 10 60 0이 출력된다. 안쪽 객체는 초기값 10으로 돌아가고 수집 목록은 비며, 바깥 트윈은 끝값 60까지 이동한다. 추가로 ctx.revert()를 ctx.kill()로 바꾸면 초기값 복원 검증이 실패하는 이유를 소스의 분기와 연결해 설명한다.

13관련 기술 심화 학습 로드맵

6주 동안 실행 결과에서 내부 구조로, 다시 브라우저 생명주기로 연결한다

매주 예상하기 → 소스 읽기 → 결과와 대조하기를 반복한다. 앞의 객체 실습으로 시간과 값의 관계를 익힌 뒤, 브라우저의 스타일·스크롤·레이아웃을 다루는 플러그인으로 범위를 넓힌다.

주차배울 개념이 레포에서 읽을 곳결과물
1주차 자바스크립트 객체 속성, 모듈 진입점, 실행 코드와 타입 선언의 차이 package.json의 exports
src/index.js
dist/gsap.js
types/index.d.ts
import·require·타입 선언이 연결되는 경로표와 과제 1의 진행률 표
2주차 상속, 중첩 타임라인, 라벨과 위치 매개변수, 재생 위치 탐색 src/gsap-core.js의 Animation·Tween·Timeline·_parsePosition
types/animation.d.ts
types/timeline.d.ts
과제 2의 두 트윈을 시간축에 그린 배치도. 절대 시각·라벨·"<"로 배치할 때의 차이 설명
3주차 보간, 이징, 연결 리스트, 일반 속성값과 CSS 변환의 처리 차이 src/gsap-core.js의 PropTween·_renderPlain
src/CustomEase.js
src/CSSPlugin.js의 _parseTransform·_renderCSSTransforms
과제 3의 곡선 비교표와 x가 일반 객체에 적용되는 경로, DOM의 transform으로 적용되는 경로의 비교도
4주차 스크롤 좌표를 진행률로 바꾸기, 구간 고정, 위치 재측정 src/ScrollTrigger.js의 _parsePosition·scrubDuration·refresh
src/Observer.js
types/scroll-trigger.d.ts
start·end·scrub·pin의 역할표와 스크롤 위치에서 애니메이션 진행률까지의 계산 흐름
5주차 레이아웃 측정, 좌표 변환 행렬, 변경 전후 상태 비교 src/Flip.js의 getState()·from()
src/utils/matrix.js
types/flip.d.ts
카드 하나를 다른 부모로 옮기는 상황의 상태표. 상태 저장 → DOM 변경 → Flip.from() 순서와 좌표 변환이 필요한 이유 설명
6주차 애니메이션 소유 범위, 복원, 화면 조건 변화에 따른 재생성, 공용 틱의 수명 src/gsap-core.js의 Context·MatchMedia·_onMediaChange·_ticker·Timeline.updateRoot
types/gsap-core.d.ts
과제 4의 복원 결과와 화면 조건이 바뀔 때의 생성·복원 순서도. autoSleep의 절전 조건과 수동 ticker.sleep()의 차이 설명
학습을 잇는 질문

“이 값은 언제 계산되고, 누가 대상에 적용하며, 누가 나중에 정리하는가?”를 매주 같은 기준으로 묻는다. 시간은 코어, 스타일·스크롤·레이아웃은 각 플러그인, 정리 범위는 컨텍스트에 연결하면 파일들이 하나의 실행 흐름으로 보인다.

14핵심 키워드 사전

API 이름을 외우기 전에, 시간·값·확장·정리의 역할을 구분한다
트윈 · Tween
대상의 속성을 일정 시간 동안 바꾸는 애니메이션 단위다. src/gsap-core.js의 Tween이 이를 구현하며, 과제의 gsap.to()가 트윈을 만든다.
보간
시작값과 끝값 사이에서 현재 값을 계산하는 일이다. src/gsap-core.js의 _renderPlain은 시작값에 변화량과 보간 비율의 곱을 더해 일반 속성값을 갱신한다.
이징
같은 시간이 흘러도 값이 빨리 또는 느리게 바뀌도록 진행 곡선을 정하는 함수다. ease: "none"은 선형 보간을 쓰며, src/CustomEase.js는 사용자 곡선을 등록하는 기능을 제공한다.
타임라인 · Timeline
트윈과 다른 타임라인을 순서·간격·겹침에 맞춰 담는 시간표다. src/gsap-core.js의 Timeline은 Animation을 상속하므로 묶음 전체도 탐색하거나 일시정지할 수 있다.
위치 매개변수
타임라인에 새 애니메이션을 언제 넣을지 지정하는 값이다. 코어의 _parsePosition은 숫자 시각, "start+=0.5" 같은 라벨 기준 시각, 직전 애니메이션의 시작을 뜻하는 "<" 등을 해석한다.
진행률 · progress()
애니메이션 한 회차에서 시간이 얼마나 진행됐는지를 0부터 1까지 나타내며, 이징이 적용된 속성값의 비율과는 구분된다. types/animation.d.ts에는 현재 진행률 읽기와 특정 진행률로 이동하기가 각각 선언돼 있다.
틱 · ticker
시간을 갱신하고 등록된 함수를 부르는 공용 박자다. src/gsap-core.js는 _ticker에 Timeline.updateRoot를 등록해 코어 애니메이션 트리를 갱신한다.
속성 트윈 · PropTween
속성 하나의 시작값·변화량·적용 함수를 담는 내부 노드다. src/gsap-core.js의 PropTween은 연결 리스트로 이어져, 하나의 트윈이 여러 속성을 갱신할 수 있게 한다.
플러그인 등록 · registerPlugin()
코어에 확장 도구를 알려 연결하는 절차다. src/index.js는 CSSPlugin을 등록하고, 사용자 코드는 사용할 CustomEase·ScrollTrigger·Flip 등을 명시적으로 등록할 수 있다.
컨텍스트 · Context
특정 실행 범위에서 생성된 애니메이션을 모아 관리하는 묶음이다. src/gsap-core.js의 Context는 수집 목록을 관리하며 getTweens()로 포함된 트윈을 확인하게 해 준다.
복원 · revert()
GSAP가 기록한 애니메이션 적용 전 상태로 돌리고 정리하는 동작이다. 코어의 Context.revert()는 복원 옵션을 넘겨 kill()을 호출하며, 복원 옵션 없는 ctx.kill()의 단순 중단과 구별된다.
반응형 조건 · matchMedia()
화면 너비 같은 미디어 조건에 맞춰 애니메이션을 만들고 정리하는 기능이다. src/gsap-core.js의 MatchMedia와 _onMediaChange는 조건 변화에 맞춰 관련 컨텍스트를 복원하고 맞는 조건의 코드를 다시 실행한다.
스크럽 · scrub
스크롤 위치를 애니메이션 진행률에 연결하는 설정이다. src/ScrollTrigger.js는 직접 연결과 부드럽게 따라가는 동작을 처리하며, types/scroll-trigger.d.ts는 설정값으로 참·거짓 또는 숫자를 허용한다.
구간 고정 · pin
지정한 스크롤 구간 동안 요소를 화면의 일정 위치에 유지하는 설정이다. src/ScrollTrigger.js는 고정 요소와 자리를 확보하는 요소의 상태를 관리해 주변 레이아웃도 함께 조정한다.
레이아웃 전환 기법 · FLIP
레이아웃 변경 전후의 위치·크기 차이를 계산해 전환을 애니메이션으로 잇는 기법이다. src/Flip.js에서는 Flip.getState()로 상태를 저장하고 실제 DOM을 바꾼 뒤 Flip.from()으로 이전 상태에서 새 상태로 이어 준다.