GSAP의 to, from, fromTo로 기본 애니메이션을 어떻게 만드나요?
GSAP의 to는 목표 상태, from은 시작 상태, fromTo는 시작과 끝을 지정해 애니메이션을 만듭니다. 로딩 실패나 움직임 감소 설정이 있어도 작업 추가 기능은 계속 사용할 수 있게 구분합니다.
적용 조건·범위: 애니메이션 완료 여부가 작업 저장이나 완료 상태의 성공 조건이 되어서는 안 됩니다.
공식 문서 참고: GSAP: to·from·fromTo와 timeline 시작하기 · MDN: prefers-reduced-motion으로 모션 감소 설정 확인
독립 실습: GSAP 선택 효과
GSAP이 처음일 때 기초 사용법: 설치부터 기본 애니메이션까지 · 독립 실습
목록 애니메이션과 코드의 GSAP·감소 모션 분기를 확인하세요.
예제 크게 보기 (새 탭)독자 질문: GSAP이 로드되지 않거나 모션 감소 설정이 켜져도 작업 추가가 가능한가?
index.html과 app.js를 같은 폴더에 저장하고 브라우저에서 여세요. CDN이 실패해도 작업 추가 기능은 동작하도록 작성했습니다.
index.html 전체
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8"><title>선택 애니메이션</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
<script src="./app.js" defer></script>
</head>
<body>
<button id="add" type="button">작업 추가</button>
<ul id="tasks"></ul>
</body>
</html>
app.js 전체
const list = document.querySelector('#tasks');
let nextId = 1;
document.querySelector('#add').addEventListener('click', () => {
const item = document.createElement('li');
item.textContent = `작업 ${nextId++}`;
list.append(item);
const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduceMotion || !globalThis.gsap) return;
globalThis.gsap.from(item, { opacity: 0, y: 8, duration: 0.25 });
});
예상 결과: 버튼을 클릭할 때마다 작업이 한 개씩 추가됩니다. GSAP 로드와 모션 허용 시 새 항목에 짧은 등장 효과가 적용됩니다. CDN을 차단하거나 모션 감소 설정을 켜면 항목은 즉시 추가되고 효과만 생략됩니다.
직접 풀어볼 문제
CDN script 태그를 잠시 지우고 작업 추가 버튼을 눌러 기능이 유지되는지 확인하세요. 다시 script를 복구한 뒤 모션 감소 에뮬레이션을 비교하세요.
검증 상태: CDN 수신, 브라우저 애니메이션, 모션 감소 설정 에뮬레이션은 미실행·미확인입니다. TypeScript의 필수 시작점은 14회차 코어입니다.
GSAP은 상태 관리 도구가 아닙니다
GSAP은 값과 DOM 속성의 시간에 따른 변화를 다루는 애니메이션 라이브러리입니다.
TaskFlow에서는 store의 데이터가 먼저 바뀌고 DOM이 렌더링된 뒤 시각적 전환만 GSAP에 맡깁니다.
사용자 이벤트
→ store 상태 변경
→ render
→ GSAP 시각 효과
애니메이션 완료 여부가 작업 저장이나 완료 상태의 성공 조건이 되어서는 안 됩니다.
CDN으로 최소 연결
별도 정적 HTML에서 시작하는 최소 실습은 CDN 스크립트를 사용합니다. 아래 독립 예제의 두 파일에는 필요한 DOM과 클릭 처리도 함께 제공됩니다.
index.html의 app module보다 앞에 추가합니다.
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
<script type="module" src="./js/app.js"></script>
실제 운영에서는 프로젝트의 빌드 도구와 패키지 관리 기준에 맞춰 npm 설치 방식을 선택할 수 있습니다.
gsap.to()
현재 상태에서 목표 상태로 이동합니다.
gsap.to('.box', {
x: 80,
duration: 0.4,
ease: 'power2.out',
});
gsap.from()
지정한 시작 상태에서 현재 CSS 상태로 들어옵니다.
gsap.from('.box', {
opacity: 0,
y: 12,
duration: 0.3,
});
새로 렌더링된 작업 항목의 등장 효과에 적합합니다.
gsap.fromTo()
시작과 끝을 모두 명시해야 할 때 사용합니다.
gsap.fromTo(
'.box',
{ opacity: 0 },
{ opacity: 1, duration: 0.3 },
);

timeline은 순서가 있는 여러 애니메이션에 사용합니다
const timeline = gsap.timeline();
timeline
.from('.title', { opacity: 0, y: 10 })
.from('.description', { opacity: 0 }, '-=0.2');
한 요소에 짧은 효과 하나만 필요하다면 timeline까지 도입할 필요는 없습니다.
prefers-reduced-motion 확인
사용자가 움직임 감소를 선호하면 애니메이션을 줄이거나 생략합니다.
const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
기능 자체를 제거하는 것이 아니라 시각 효과만 생략합니다.
라이브러리가 없어도 앱은 동작해야 합니다
CDN 요청이 실패하거나 GSAP이 로드되지 않을 수 있습니다.
if (!globalThis.gsap) {
return;
}
선택 효과가 핵심 데이터 흐름을 막지 않도록 방어합니다.
자주 발생하는 오류
렌더링 전에 없는 요소를 선택합니다
TaskFlow는 renderTasks()가 DOM을 만든 뒤 애니메이션 함수를 호출합니다.
GSAP callback에서 store 상태를 바꿉니다
애니메이션 모듈은 상태 소유자가 아닙니다. 상태 변경은 기존 store 흐름에서 끝낸 뒤 시각 효과만 실행합니다.
reduced motion을 무시합니다
장식 목적의 움직임은 사용자의 OS 설정을 존중해야 합니다.
학습 완료 체크리스트
- GSAP을 정적 프로젝트에 연결할 수 있습니다.
to,from,fromTo차이를 설명할 수 있습니다.- timeline이 필요한 경우를 구분할 수 있습니다.
- reduced motion 설정을 확인할 수 있습니다.
- GSAP 없이도 핵심 기능을 유지할 수 있습니다.
독립 실습은 여기서 마칠 수 있습니다. 아래 선택 실습은 TaskFlow 프로젝트 파일로 진행합니다.
선택 실습: 할 일 앱에 적용하기
TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 JavaScript로 화면에 동작을 붙입니다.
14회차의 상태·요청 처리 흐름은 유지하고 animation.js 와 app의 호출 한 줄만 선택적으로 추가합니다.
선택: 연재 프로젝트와 전체 자료
연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.
TaskFlow 선택 적용
GSAP이 처음일 때 기초 사용법: 설치부터 기본 애니메이션까지 · 단계별 TaskFlow
작업 추가·완료 전환·삭제와 현재 단계의 필터/출력을 확인하세요. 검색 조건을 바꿔 가상 URL을 확인하세요.
예제 크게 보기 (새 탭)TaskFlow의 핵심 기능은 14회차에서 이미 완성되었습니다.
이번 글은 필수 기능을 더하는 단계가 아니라 외부 애니메이션 라이브러리를 처음 도입하는 방법을 배우는 선택 확장입니다. GSAP이 없어도 추가·완료·삭제·검색·fetch 기능은 모두 정상이어야 합니다.
변경 설명과 부분 예제를 읽은 뒤, 시작·완료 파일과 적용 순서에서 필요한 선언·import·화면 연결을 포함한 전체 파일을 확인하세요. 이번 단계의 변경 이유와 적용 위치도 함께 확인합니다.
시작본과 이번 변경
JavaScript 고급 비동기: AbortController와 최신 요청 경쟁 제어의 완료 체크포인트를 시작본으로 사용합니다.
index.html에서 app module 앞에 GSAP script를 연결하고 js/animation.js를 만듭니다. app.js의 기존 render 마지막에 animateTaskList 호출을 추가합니다. CDN 차단과 reduced motion 상태에서도 추가·완료·삭제가 유지되는지 확인합니다.
연속 프로젝트 선수 조건
javascript-step-14-final이 정상 동작해야 합니다.- DOM 선택과 렌더링 흐름을 이해하고 있어야 합니다.
- 이 글은 필수가 아닌 선택 확장입니다.
이전 체크포인트
javascript-step-14-final
이번 회차 변경 코드와 전체본 연결
14회차의 상태·요청 처리 흐름은 유지하고 animation.js와 app의 호출 한 줄만 선택적으로 추가합니다.
전체 파일·코드 보기
js/animation.js
function shouldReduceMotion() {
return matchMedia('(prefers-reduced-motion: reduce)').matches;
}
export function animateTaskList() {
if (shouldReduceMotion()) return;
if (!globalThis.gsap) return;
const items = document.querySelectorAll('#task-list [data-task-id]');
globalThis.gsap.from(items, {
opacity: 0,
y: 8,
duration: 0.25,
stagger: 0.03,
ease: 'power2.out',
});
}
js/app.js 적용
import { animateTaskList } from './animation.js';
function render() {
const controls = getTaskControls();
const tasks = store.filterTasks(controls.status, controls.query);
renderTasks(tasks, store.getTaskSummary());
animateTaskList();
}
실제 14회차의 서버 요청 기반 필터 흐름을 유지한다면 최신 응답을 store에 반영한 뒤 호출되는 기존 render() 안에서 동일하게 애니메이션이 실행됩니다.
GSAP 로드 실패 검증
- CDN script를 임시로 제거
- 페이지 새로고침
- 작업 추가·완료·삭제·검색 기능 확인
- Console에 GSAP 관련 치명적 오류가 없는지 확인
- CDN script 복구
reduced motion 검증
운영체제 또는 DevTools에서 prefers-reduced-motion: reduce를 에뮬레이션하고 작업 목록이 즉시 표시되는지 확인합니다.
중간부터 시작하는 전체 파일과 적용 순서
시작 ZIP은 직전 프로젝트 단계 javascript-step-14-final의 실제 완료 파일입니다. 프로젝트 순서를 이어가는 기준이며 사이트의 기술 읽기 순서와는 구분합니다.
- 시작 ZIP을 새 폴더에 풉니다. 이 폴더가 작업 루트이며 기존 체크포인트 파일을 별도로 찾을 필요가 없습니다.
- animation 모듈과 render 호출, 본문의 GSAP CDN script를 추가합니다. GSAP가 없거나 reduced motion이면 즉시 반환합니다.
- 아래 변경 파일을 같은 상대 경로에 추가하거나 전체 교체합니다. 표시하지 않은 파일과 images는 시작본 그대로 유지합니다. package-lock.json은 완료 ZIP에 있는 파일을 함께 적용합니다.
- 작업 루트에서 명령을 실행하고 예상 결과를 확인합니다. 완료 ZIP은 직접 적용한 결과를 비교하는 기준입니다.
추가·교체 파일
index.html— 전체 교체CHANGES.md— 전체 교체README.md— 전체 교체js/app.js— 전체 교체js/animation.js— 추가
실행과 확인
npm start
npm test
개발 서버는 실행 상태로 두고 다른 터미널에서 검사 명령을 실행하세요. Node.js 24.18.0에서 검증했습니다. JavaScript는 http://127.0.0.1:4173, TypeScript는 Vite 실행 출력의 주소로 접속합니다.
예상 결과: GSAP 로드 성공 시 목록이 순차 등장하고 로드 실패·reduced motion에서 CRUD는 유지됩니다. 실제 브라우저 애니메이션과 CDN 가용성은 별도 확인 대상입니다.
실제 검증: JavaScript 모듈·DOM·경계값 실행 검사 통과. 시작 ZIP에 이 글의 변경 파일만 적용한 결과와 완료 ZIP의 모든 파일 내용이 일치했습니다. 브라우저 화면 배치·실제 기기·외부 CDN 가용성은 이 검사에 포함하지 않았습니다.
이 절의 전체 파일은 ZIP과 일치합니다. 위쪽 원문의 짧은 예제나 생략 표시가 있는 부분 코드는 개념·변경 위치 설명용입니다. 서버 저장과 인증은 제공하지 않으며 JSON fixture는 로컬 읽기 실습용입니다.
복사할 변경 파일 전체 코드
index.html — 전체 파일
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>
TaskFlow - 간단한 작업 정리 서비스
</title>
<link href="./style.css" rel="stylesheet"/>
<meta content="TaskFlow의 주요 기능과 요금제, 문의 방법을 확인할 수 있는 작업 관리 서비스 소개 페이지입니다." name="description"/>
</head>
<body>
<a class="skip-link" href="#main">
본문으로 바로가기
</a>
<header class="site-header">
<div class="page site-header__inner">
<a class="brand" href="#home">
TaskFlow
</a>
<nav aria-label="주요 메뉴" class="nav">
<a href="#features">
기능
</a>
<a href="#pricing">
요금제
</a>
<a href="#contact">
문의
</a>
<a href="#faq">
FAQ
</a>
</nav>
</div>
</header>
<main class="page" id="main" tabindex="-1">
<div class="service-layout">
<div>
<section class="hero" id="home">
<div>
<h1>
해야 할 일을 한눈에 정리하세요
</h1>
<p>
TaskFlow는 오늘의 작업을 빠르게 정리하는 서비스입니다.
</p>
<a class="button-link" href="#contact">
시작 문의하기
</a>
</div>
<img alt="TaskFlow 서비스 소개 페이지의 기능·문의 화면" class="hero__image" height="800" src="./images/hero.webp" width="1200"/>
</section>
<section id="features">
<h2>
주요 기능
</h2>
<div class="feature-grid">
<article class="card feature-card">
<span class="feature-card__badge">
NEW
</span>
<h3>
빠른 작업 추가
</h3>
<p>
해야 할 일을 바로 기록할 수 있습니다.
</p>
<img alt="TaskFlow 기능 카드 세 개" class="feature-card__image" height="450" loading="lazy" src="./images/feature-board.webp" width="800"/>
</article>
<article class="card">
<h3>
간단한 분류
</h3>
<p>
해야 할 일을 구분합니다.
</p>
</article>
<article class="card">
<h3>
진행 확인
</h3>
<p>
완료 여부를 확인합니다.
</p>
</article>
<article class="card task-preview">
<div class="task-preview__content">
<h3>
아주 긴 작업 제목과 URL
</h3>
<p>
https://example.com/very-long-task-url-without-normal-break-points-123456789
</p>
</div>
<a href="#contact">
문의
</a>
</article>
</div>
</section>
<section class="faq" id="faq">
<h2>
자주 묻는 질문
</h2>
<details>
<summary>
TaskFlow는 어떤 서비스인가요?
</summary>
<p>
해야 할 일을 빠르게 정리하는 작업 관리 서비스 예제입니다.
</p>
</details>
<details>
<summary>
모바일에서도 볼 수 있나요?
</summary>
<p>
작은 화면부터 설계한 반응형 레이아웃을 사용합니다.
</p>
</details>
<details>
<summary>
문의는 어디서 하나요?
</summary>
<p>
아래 문의 폼을 이용할 수 있습니다.
</p>
</details>
</section>
<section id="pricing">
<h2>
요금제
</h2>
<div class="pricing-grid">
<article class="card">
<h3>
Basic
</h3>
<p>
개인 학습용
</p>
</article>
<article class="card">
<h3>
Pro
</h3>
<p>
더 많은 프로젝트
</p>
</article>
</div>
<div aria-label="요금제 비교표 가로 스크롤" class="table-scroll" role="region" tabindex="0">
<table class="pricing-table">
<caption>
TaskFlow 요금제 기능 비교
</caption>
<thead>
<tr>
<th scope="col">
항목
</th>
<th scope="col">
Basic
</th>
<th scope="col">
Pro
</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">
프로젝트
</th>
<td>
3개
</td>
<td>
무제한
</td>
</tr>
<tr>
<th scope="row">
작업 보관
</th>
<td>
30일
</td>
<td>
무제한
</td>
</tr>
<tr>
<th scope="row">
팀 공유
</th>
<td>
없음
</td>
<td>
지원
</td>
</tr>
</tbody>
</table>
</div>
</section>
<section id="contact">
<h2>
문의하기
</h2>
<form>
<div class="form-field">
<label for="name">
이름
</label>
<input autocomplete="name" id="name" name="name" type="text"/>
</div>
<div class="form-field">
<label for="email">
이메일
</label>
<input autocomplete="email" id="email" name="email" type="email"/>
</div>
<div class="form-field">
<label for="message">
문의 내용
</label>
<textarea id="message" name="message" rows="5"></textarea>
</div>
<button type="submit">
문의 보내기
</button>
</form>
</section>
<div class="feature-slot">
<article class="card feature-card">
<img alt="TaskFlow 기능 카드 세 개" class="feature-card__image" height="450" loading="lazy" src="./images/feature-board.webp" width="800"/>
<div>
<h3>
작업 카드
</h3>
<p>
넓은 공간에서는 이미지와 설명을 나란히 배치합니다.
</p>
</div>
</article>
</div>
</div>
<aside aria-label="기능 미리보기">
<div class="feature-slot">
<article class="card feature-card">
<img alt="TaskFlow 기능 카드 세 개" class="feature-card__image" height="450" loading="lazy" src="./images/feature-board.webp" width="800"/>
<div>
<h3>
작업 카드
</h3>
<p>
넓은 공간에서는 이미지와 설명을 나란히 배치합니다.
</p>
</div>
</article>
</div>
</aside>
</div>
<section
class="task-demo"
id="task-demo"
aria-labelledby="task-demo-title"
>
<h2 id="task-demo-title">작업 관리 데모</h2>
<p>JavaScript 과정에서 이 영역을 단계적으로 동작하게 만듭니다.</p>
<form class="task-form" id="task-form" novalidate>
<div class="form-field">
<label for="task-title">작업 이름</label>
<input
id="task-title"
name="title"
type="text"
maxlength="80"
required
aria-describedby="task-message"
/>
</div>
<button type="submit">작업 추가</button>
<div class="form-field">
<label for="task-due-at">마감 시각</label>
<input
id="task-due-at"
name="dueAt"
type="datetime-local"
/>
</div>
</form><div class="task-controls">
<label for="task-status">상태</label>
<select id="task-status">
<option value="all">전체</option>
<option value="pending">미완료</option>
<option value="completed">완료</option>
</select>
<label for="task-search">검색</label>
<input id="task-search" type="search" />
</div>
<p id="task-message" aria-live="polite"></p>
<div id="task-summary" aria-live="polite"></div>
<ul class="task-list" id="task-list"></ul>
</section>
<p id="task-load-state" aria-live="polite"></p></main>
<footer class="site-footer">
<div class="page">
<p>
© TaskFlow
</p>
</div>
</footer>
<a class="help-button" href="#contact">
문의
</a>
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
<script type="module" src="./js/app.js"></script></body>
</html>
js/app.js — 전체 파일
import { animateTaskList } from './animation.js';
import { loadTasks } from './task-service.js';
import { renderLoadState } from './task-view.js';
import { createTaskStore } from './task-store.js';
const store = createTaskStore();
import {
bindTaskControls,
bindTaskForm,
bindTaskList,
getTaskControls,
renderTasks,
setTaskControls,
} from './task-view.js';
import { readUrlState, writeUrlState } from './url-state.js';
function render() {
const controls = getTaskControls();
const tasks = store.filterTasks(controls.status, controls.query);
renderTasks(tasks, store.getTaskSummary());
animateTaskList();
}
const initialUrlState = readUrlState();
setTaskControls(initialUrlState);
bindTaskForm(({ title, dueAt }) => {
store.addTask(title, dueAt);
render();
});
bindTaskControls(() => {
const controls = getTaskControls();
writeUrlState(controls);
loadLatestTasks();
});
bindTaskList(({ id, action }) => {
if (action === 'toggle') store.toggleTask(id);
if (action === 'delete') store.deleteTask(id);
render();
});
render();
let currentController = null;
let latestTicket = 0;
async function loadLatestTasks() {
currentController?.abort();
const controller = new AbortController();
currentController = controller;
const ticket = ++latestTicket;
const controls = getTaskControls();
renderLoadState('작업을 불러오는 중입니다.');
try {
const tasks = await loadTasks({
status: controls.status,
query: controls.query,
signal: controller.signal,
});
if (ticket !== latestTicket) {
return;
}
store.replaceTasks(tasks);
render();
renderLoadState('최신 작업을 표시했습니다.');
} catch (error) {
if (ticket !== latestTicket || error.name === 'AbortError') {
return;
}
console.error(error);
renderLoadState('작업을 불러오지 못했습니다.');
} finally {
if (ticket === latestTicket) {
currentController = null;
}
}
}
loadLatestTasks();
js/animation.js — 전체 파일
function shouldReduceMotion() {
return matchMedia('(prefers-reduced-motion: reduce)').matches;
}
export function animateTaskList() {
if (shouldReduceMotion()) return;
if (!globalThis.gsap) return;
const items = document.querySelectorAll('#task-list [data-task-id]');
globalThis.gsap.from(items, {
opacity: 0,
y: 8,
duration: 0.25,
stagger: 0.03,
ease: 'power2.out',
});
}
완료 체크포인트
javascript-step-15-gsap
이 체크포인트는 선택 결과물입니다.
다음 학습
TypeScript 과정은 GSAP을 포함한 상태가 아니라 JavaScript 코어 최종본을 기준으로 시작합니다.
javascript-step-14-final
→ TypeScript
animation.js는 원한다면 이후 프로젝트에서도 유지할 수 있지만 타입 전환의 필수 대상은 아닙니다.
글 소개·학습 준비·전체 목차

JavaScript · 15 / 15 · 선택 확장 · GSAP 기초
정적 HTML에 GSAP 스크립트를 연결한 뒤 DOM 요소를 선택하고, to로 목표 상태를, from으로 시작 상태를, fromTo로 시작과 끝을 지정해 시간에 따른 변화를 만듭니다. 두 파일의 작은 예제에서 항목 추가 뒤 등장 효과와 모션 감소 처리를 확인합니다.
이번 글에서 배우는 내용
- GSAP 연결
gsap.to()gsap.from()gsap.fromTo()- duration과 ease
- timeline 사용 기준
- DOM 대상 선택
prefers-reduced-motion- 라이브러리 실패 시 기능 유지
먼저 알아야 할 내용
HTML의 script 연결, DOM 선택과 클릭 이벤트를 알면 됩니다. GSAP을 처음 사용해도 아래 두 파일과 CDN 연결로 단독 실습이 가능합니다.
목차
이 글이 도움이 되었나요?
읽음 기록
읽음 — / 전체 15개 · 필수 14개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.