들어가며

웹이나 앱을 사용하다 보면 자연스럽게 눈길을 끄는 애니메이션을 마주할 때가 있다. 애니메이션은 재미를 더할 뿐 아니라 사용자의 이해를 돕고 인터랙션의 결과를 명확히 전달하며 브랜드의 개성을 표현하는 등 중요한 역할을 한다.

그런데 이런 애니메이션을 직접 만들어야 하는 순간이 오면 이야기가 달라진다. 디자이너가 건네준 프로토타입 영상을 보며 "이걸 어떻게 구현하지?"라고 막막해한 경험이 한 번쯤은 있을 것이다. 혹은 머릿속에 그려지는 움직임은 있지만 코드로 옮기려니 어디서부터 손을 대야 할지 모를 때도 있다.

핵심은 애니메이션도 설계할 수 있다는 것이다. 복잡해 보이는 움직임도 잘게 쪼개면 단순한 상태 변화의 조합이고, 각각의 상태 변화는 수학적으로 표현할 수 있다. 이 글에서는 애니메이션을 체계적으로 분해하고 설계하는 방법을 다룰 것이다.

애니메이션은 그래프다

무언가를 "설계"하려면 두 가지 필요한 조건이 있다. 재현할 수 있어야 하고, 조합할 수 있어야 한다. 머릿속의 감각에 의존하면 같은 움직임을 다시 만들기 어렵고, 여러 움직임을 체계적으로 엮기도 어렵다. 따라서 움직임을 공학적으로 나타내는 방법이 필요하며, 이를 위해 그래프를 이용할 수 있다. 모든 애니메이션은 그래프로 표현할 수 있다. 이 관점을 갖게 되면 복잡한 움직임도 체계적으로 분석하고 만들어낼 수 있게 된다.

요소의 투명도가 0에서 1로 변하는 페이드인 애니메이션을 생각해보자. 이것을 그래프로 그리면 가로축은 시간, 세로축은 투명도가 된다. 예를 들면, 0초에서 시작해 2초에 끝나는 동안 투명도 값이 0에서 1로 올라가는 그래프를 그려 볼 수 있다.

페이드인 — 시간에 따라 투명도가 0에서 1로 변한다

이동 애니메이션도 마찬가지다. 왼쪽에서 오른쪽으로 움직이는 요소는 시간-위치 그래프로 나타낼 수 있고, 크기가 커지는 애니메이션은 시간-스케일 그래프가 된다. 가로축이 꼭 시간일 필요도 없다. 스크롤 위치에 따라 요소가 나타나는 패럴랙스 효과는 스크롤 오프셋이 가로축이 된다. 어떤 애니메이션이든 결국 어떤 입력에 따라 값이 변하는 것이고, 이 변화를 그래프로 그릴 수 있다.

여기서 중요한 것은 그래프의 모양이 곧 움직임의 느낌을 결정한다는 점이다. 같은 시작점과 끝점이라도 곡선의 형태에 따라 전혀 다른 인상을 줄 수 있다. 이 곡선을 원하는 대로 만들어내는 것이 애니메이션 설계의 핵심이며, 그 도구가 되는 것이 수학이다.

애니메이션을 위한 도구, 수학

그렇다면 원하는 모양의 그래프를 어떻게 만들 수 있을까? 여기서 수학이 등장한다. 이 섹션에서는 애니메이션에서 자주 쓰이는 수학적 도구들을 살펴보고 그것들이 어떻게 그래프의 형태를 조절하는지 알아볼 것이다. 수학이 낯설게 느껴질 수도 있지만 걱정할 필요 없다. 핵심은 수학적 개념을 이해하는 것이 아니라 애니메이션의 느낌에 어떤 영향을 주는지 파악하는 것이다.

이징과 베지어 커브

가장 단순한 애니메이션은 선형이다. 선형은 시작점에서 끝점까지 일정한 속도로 변한다. 하지만 현실 세계의 움직임은 선형이 아니다. 공을 던지면 처음에 빠르다가 점점 느려지고, 자동차는 출발할 때 천천히 시작해서 속도를 높인다. 이런 자연스러운 가감속을 표현하는 것이 이징easing 함수다.

이징 함수는 0에서 1 사이의 진행률을 입력받아 변환된 진행률을 출력한다. ease-in은 느리게 시작해서 빠르게 끝나고, ease-out은 빠르게 시작해서 느리게 끝난다. 이것을 수학적으로 표현하는 가장 널리 쓰이는 방법이 큐빅 베지어 커브cubic bezier curve다.

큐빅 베지어 커브는 네 개의 제어점으로 곡선을 정의한다. 이징에서는 시작점 (0, 0)과 끝점 (1, 1)이 고정이므로 실질적으로 두 개의 제어점 (x1, y1)과 (x2, y2)만 조절하면 된다. 제어점은 곡선이 지나가는 점이 아니라, 곡선을 끌어당기는 점이다. 마치 자석처럼 곡선의 경로에 영향을 준다.

그렇다면 제어점으로부터 곡선이 어떻게 만들어질까? 핵심은 선형 보간linear interpolation의 재귀적 적용이다. 두 점 사이의 선형 보간은 단순하다. 진행률 t가 0이면 시작점, 1이면 끝점, 0.5면 정확히 중간점이다.

function lerp(a, b, t) {
  return a + (b - a) * t;
}

베지어 커브는 이 보간을 여러 단계에 걸쳐 반복한다. 네 개의 제어점 P0, P1, P2, P3이 있다고 하자.

  1. P0-P1, P1-P2, P2-P3 사이를 각각 t만큼 보간하여 세 개의 중간점을 얻는다
  2. 그 세 중간점 사이를 다시 t만큼 보간하여 두 개의 점을 얻는다
  3. 마지막 두 점 사이를 t만큼 보간하면 최종 곡선 위의 한 점이 나온다

이 방식은 꽤나 강력한데 두 개의 제어점만으로 매우 다양한 움직임을 만들어낼 수 있기 때문이다. 그 중에서 대표적으로 많이 사용되는 제어점 조합을 예제로 살펴보면 다음과 같다.

cubic-bezier(0.25, 0.1, 0.25, 1)
← 이 공의 위치가 커브를 따라 변합니다 →

이징을 실전에서 어떻게 선택할까? 토스트 알림이 화면 아래에서 올라오는 애니메이션을 예로 들어보자.

ease-out을 적용하면 토스트가 빠르게 나타나 사용자의 주의를 끌고, 최종 위치에서 부드럽게 감속하며 안착한다. 반면 ease-in을 적용하면 천천히 시작해서 점점 빨라지는데, 등장이 느긋해서 덜 눈에 띈다. linear는 일정한 속도로 올라오다 갑자기 멈추기 때문에 기계적인 느낌을 준다.

같은 요소, 같은 움직임이지만 이징에 따라 전달하는 인상이 완전히 달라진다. 어떤 이징이 "정답"인 것은 아니다. 중요한 것은 그 움직임이 어떤 목적을 가지는가다. 사용자의 시선을 빠르게 끌어야 한다면 초반이 빠른 커브를, 조용히 사라져야 한다면 후반이 빠른 커브를 선택할 수 있다. 아래 데모에서 같은 토스트에 서로 다른 이징을 적용했을 때 인상이 어떻게 달라지는지 비교해보자.

Linear
저장되었습니다
Ease In
저장되었습니다
Ease Out
저장되었습니다
Ease In-Out
저장되었습니다
같은 토스트가 이징에 따라 어떻게 다른지 비교해보세요

베지어 커브의 원리를 이해하면 도구에 의존하지 않고도 원하는 움직임을 직접 설계할 수 있다. "처음에 빠르게 튀어나왔다가 천천히 안착하는 느낌"이 필요하면 첫 번째 제어점의 y값을 크게, 두 번째 제어점을 (1, 1) 근처에 두면 된다. 그래프의 모양을 먼저 그린 뒤, 그에 맞는 제어점을 찾아가는 것이다.

지수적 접근

이징은 시작과 끝이 정해진 곡선이다. 하지만 목표값이 도중에 바뀔 수 있는 상황에서는 어떨까? 커서를 따라다니는 요소처럼 목표가 매 프레임 달라진다면, 미리 정해진 곡선으로는 대응하기 어렵다. 이때 유용한 패턴이 지수적 접근exponential approach이다.

value += (target - value) * factor; // factor: 0~1

수식은 놀라울 정도로 단순하다. 위 수식을 매 프레임마다 적용하면 현재 값과 목표 값의 차이에 비례하는 만큼만 이동한다. 그 결과 빠르게 접근하다가 점점 느려지는 자연스러운 감속 곡선이 만들어진다.

지수적 접근 — factor가 클수록 빠르게 목표에 수렴한다

factor가 작으면 천천히 다가가고 크면 빠르게 다가간다. 수학적으로 이것은 지수 감쇠exponential decay다. 차이가 매 프레임 (1 - factor) 비율로 줄어들기 때문에 남은 거리가 지수적으로 감소한다.

마우스를 움직여보세요

위치뿐 아니라 모든 종류의 값에 동일하게 적용할 수 있다. 대시보드의 카운터가 목표 수치를 향해 빠르게 올라가다 점점 느려지며 안착하는 효과도 같은 원리다.

총 방문자 수
0
목표값을 바꿔보세요 — 숫자가 부드럽게 새 목표를 쫓아간다

프로그레스 바도 마찬가지다. 실제 진행률은 네트워크 상황에 따라 뚝뚝 끊기지만 표시되는 바가 지수적 접근으로 목표를 쫓아가면 사용자에게는 부드러운 진행으로 보인다.

파일 업로드0%
실제 진행률이 뚝뚝 끊겨도 바는 부드럽게 목표를 쫓아간다

지수적 접근의 장점은 목표가 바뀌어도 자연스럽다는 것이다. 커서가 갑자기 반대편으로 이동해도 현재 위치에서부터 새 목표를 향해 부드럽게 방향을 전환한다.

스프링 애니메이션

이징과 지수적 접근은 목표에 단조롭게 수렴한다. 하지만 현실의 물체에는 관성이 있다. 목표를 살짝 지나쳤다가 되돌아오는 탄성이 움직임에 생동감을 더한다. 버튼을 눌렀을 때의 탄력적인 피드백, 드래그한 요소가 원래 자리로 튕겨 돌아오는 느낌을 주고 싶다면 스프링 애니메이션을 사용할 수 있다.

스프링 애니메이션은 물리의 감쇠 진동damped harmonic oscillation을 기반으로 한다. 용수철에 매달린 물체를 생각하면 된다. 물체를 잡아당겼다 놓으면 원래 위치를 중심으로 진동하고 마찰에 의해 점점 진폭이 줄어들면서 결국 멈춘다.

기반이 되는 원리는 두 가지 힘이다.

  • 복원력: 현재 위치가 목표에서 멀수록 강하게 잡아당긴다. F = -k × (현재 - 목표). 여기서 k강성stiffness이다.
  • 감쇠력: 속도에 비례하여 움직임을 억제한다. F = -c × 속도. 여기서 c감쇠 계수damping이다.

매 프레임마다 이 두 힘을 합산하여 가속도를 구하고, 가속도로 속도를 갱신하고, 속도로 위치를 갱신한다. 코드로 표현하면 다음과 같다.

const force = -stiffness * (current - target) - damping * velocity;
velocity += force * dt;
current += velocity * dt;
Underdamped (진동) — damping ratio: 0.46
↑ target

스프링의 느낌은 stiffnessdamping 두 값으로 결정된다. 강성이 높으면 팽팽하고 빠르게 반응하며 감쇠가 낮으면 더 오래 진동한다. 이 두 파라미터를 조절하는 것만으로 "톡 튕기는 버튼"부터 "부드럽게 안착하는 카드"까지 다양한 느낌을 만들어낼 수 있다.

좋아요 버튼을 예로 들어보자. 하트를 누르는 순간 스케일이 순간적으로 작아졌다가 목표(원래 크기)를 향해 튕겨 올라온다. 이때 스프링의 오버슛 덕분에 목표를 살짝 넘었다 돌아오며 "눌렀다"는 촉각적 피드백을 만든다.

하트를 눌러보세요 — 스프링이 만드는 탄성 있는 피드백

스프링 애니메이션이 이징 기반 애니메이션과 근본적으로 다른 점은 duration이 없다는 것이다. 이징은 "0.3초 동안" 같은 고정 시간이 있지만, 스프링은 물리 시뮬레이션이 수렴할 때까지 계속된다. 그리고 또 다른 점은 목표값이 중간에 바뀌면 현재 속도를 유지한 채 새 목표를 향해 자연스럽게 전환된다는 점이다.

물리 시뮬레이션

스프링은 하나의 값이 목표를 향해 진동하는 시스템이었다. 하지만 물리를 더 넓게 적용할 수 있다. 중력, 충돌, 마찰, 관성과 같은 물리 법칙들을 조합하면 직접 설계하기 어려운 복잡한 움직임을 자연스럽게 만들어낼 수 있다.

물리 시뮬레이션의 기본 구조는 단순하다. 매 프레임마다 세 단계를 반복한다.

  1. 힘 계산: 각 오브젝트에 작용하는 힘들을 합산한다 (중력, 스프링, 마찰, 사용자 입력 등)
  2. 적분: 힘으로부터 가속도를, 가속도로부터 속도를, 속도로부터 위치를 갱신한다
  3. 제약 처리: 충돌 감지, 경계 제한, 연결 관계 등의 제약 조건을 적용한다
for (const obj of objects) {
  // 힘 계산
  const gravity = { x: 0, y: 9.8 * obj.mass };
  const friction = { x: -obj.vx * drag, y: -obj.vy * drag };
  const fx = gravity.x + friction.x;
  const fy = gravity.y + friction.y;

  // 적분
  obj.vx += (fx / obj.mass) * dt;
  obj.vy += (fy / obj.mass) * dt;
  obj.x += obj.vx * dt;
  obj.y += obj.vy * dt;

  // 제약 처리 (바닥 충돌)
  if (obj.y > floorY) {
    obj.y = floorY;
    obj.vy *= -restitution; // 반발 계수만큼 튕김
  }
}

이 구조가 강력한 이유는 규칙만 정의하면 움직임은 알아서 만들어진다는 점이다. 컨페티를 예로 들어보자. 각 조각의 궤적을 이징 함수로 일일이 설계하려면 비현실적이다. 하지만 중력과 공기 저항, 회전만 설정하면 수십 개의 조각이 각자 다른 궤적을 그리며 떨어진다. 개별 궤적을 신경 쓸 필요가 없다.

규칙만 정의하면 자연스러운 움직임이 만들어진다

이런 점에서 물리 시뮬레이션은 다수의 오브젝트가 상호작용하거나, 사용자 입력에 따라 결과가 달라지는 상황에서 유용하다.

다만 물리 시뮬레이션은 예측 가능성이 떨어진다는 단점이 있다. 이징 기반 애니메이션은 언제 어떤 위치에 있을지 정확히 알 수 있지만 물리 시뮬레이션은 초기 조건에 따라 결과가 달라진다. 따라서 UI의 핵심 전환 애니메이션보다는 보조적인 효과나 인터랙티브 요소에 더 적합한 경우가 많다.

자연스러운 방향 전환

지금까지는 값이 목표를 향해 얼마나 변하는지를 다뤘다. 이번에는 어느 방향으로 변하는지를 다룰 것이다. 화면 위에서 요소가 커서를 따라다니는 애니메이션을 만든다고 하자. 위치를 따라가는 것은 어렵지 않다. 하지만 요소가 이동 방향을 바라보도록 회전까지 시키려면 어떻게 해야 할까?

이때 필요한 것이 atan2 함수다. atan2(dy, dx)는 두 점 사이의 각도를 라디안으로 반환한다. 현재 위치와 목표 위치의 차이 (dx, dy)를 넣으면, 요소가 바라봐야 할 방향이 나온다.

const dx = targetX - currentX;
const dy = targetY - currentY;
const angle = Math.atan2(dy, dx);
element.style.transform = `rotate(${angle}rad)`;

단순하지만 이것만으로도 움직임의 자연스러움이 크게 달라진다. 화살표가 이동 방향을 바라보거나, 캐릭터가 목적지를 향해 고개를 돌리거나, 파티클이 퍼져나가는 방향으로 늘어나는 효과 모두 atan2가 핵심이다.

atan2의 실전 활용 사례를 하나 보자. 카드 UI 위에서 마우스를 움직이면 카드가 커서 방향으로 기우는 3D 틸트 효과다. atan2로 커서의 방향을, 중심으로부터의 거리로 기울기의 강도를 구한다.

홍길동
Frontend Developer
애니메이션과 인터랙션에 관심이 많은 개발자입니다.
128 posts1.2k followers
카드 위에서 마우스를 움직여보세요

이처럼 두 점 사이의 방향을 구할 수 있으면 표현할 수 있는 애니메이션의 폭이 크게 넓어진다.

삼각함수로 만드는 주기적 움직임

우리가 중학교 때 배운 삼각함수는 애니메이션에서 매우 넓은 쓰임새를 가진다. 핵심 특성은 주기성이다. 값이 -1에서 1 사이를 끝없이 반복하기 때문에, 반복되는 움직임을 만드는 데 최적의 도구다.

하나의 sin 함수와 위상 오프셋만으로 수십 개 요소의 협응적인 움직임이 만들어진다. 로딩 인디케이터, 이퀄라이저 바, 물결 효과 등 주기적이면서 군무처럼 보이는 애니메이션 대부분이 이 원리 위에 있다.

const y = amplitude * Math.sin(time * frequency);

sin이나 cos 함수에 세 개의 파라미터를 조절하면 원하는 모양의 반복을 만들 수 있다.

  • 진폭amplitude: 움직임의 크기. 값이 클수록 넓게 흔들린다
  • 주파수frequency: 움직임의 빠르기. 값이 클수록 빠르게 반복한다
  • 위상phase: 시작 지점의 오프셋. 여러 요소에 서로 다른 위상을 주면 파동 효과가 만들어진다

삼각함수가 특히 강력한 순간은 여러 요소에 위상 차이를 줄 때다. 예를 들어, 리스트의 각 아이템에 인덱스에 비례하는 위상을 주면 파도처럼 차례로 움직이는 효과가 된다.

items.forEach((item, i) => {
  const y = amplitude * Math.sin(time * frequency + i * phaseOffset);
  item.style.transform = `translateY(${y}px)`;
});
위상 차이를 조절하면 파동 효과를 만들 수 있습니다

실전에서는 메신저의 타이핑 인디케이터가 대표적이다. 세 개의 점이 차례로 튀어오르는 이 효과는 동일한 sin에 위상만 다르게 준 것이다.

sin + 위상 오프셋으로 만든 타이핑 인디케이터

랜딩 페이지에서 흔히 보이는 플로팅 효과도 삼각함수를 이용해 만들 수 있다. 각 요소마다 서로 다른 진폭, 주파수, 위상을 주면 동일한 sin 함수 하나로 자연스러운 배경이 만들어진다.

🚀
🎨
💡
⚙️
🌟
Creative Studio
각 요소마다 다른 진폭·주파수·위상을 가진다

톱니파

sin은 값이 올라갔다 내려오는 왕복 운동에 적합하다. 하지만 모든 반복이 왕복인 것은 아니다. 알림 배지의 펄스 링처럼 값이 0에서 1로 한 방향으로 진행한 뒤 처음으로 돌아가 다시 시작하는 패턴도 있다. 이것을 톱니파sawtooth wave라고 부른다.

const p = (t % period) / period; // 항상 0~1, 주기마다 리셋

tperiod로 나눈 나머지를 구하면, 값은 0에서 1까지 선형으로 올라갔다가 즉시 0으로 떨어진다. 이 p를 스케일이나 투명도에 매핑하면 "퍼지면서 사라지는" 반복 효과가 만들어진다.

const scale = 1 + p * 0.8;   // 1 → 1.8로 커짐
const opacity = 1 - p;        // 1 → 0으로 사라짐
3
톱니파 — 값이 0→1로 단방향 진행 후 리셋을 반복한다

톱니파는 "시작 → 끝 → 즉시 리셋"이라는 단순한 구조 덕분에 펄스, 핑, 반복 프로그레스 같은 패턴에 널리 쓰인다. sin의 부드러운 왕복과 톱니파의 단방향 리셋 두 가지만으로도 대부분의 주기적 애니메이션을 표현할 수 있다.

애니메이션 설계하기

지금까지 이징, 스프링, 삼각함수 같은 수학 도구를 살펴봤다. 하나의 도구로 해결되는 경우도 있지만, 현실의 애니메이션은 대부분 더 복잡하다. 예를 들어, 알림이 나타날 때 배경이 어두워지면서 카드가 올라오고, 내용이 드러나도록 만들고 싶다면 어떻게 해야할까?

복잡한 애니메이션을 설계하려면 먼저 두 가지를 이해해야 한다. 그래프를 어떻게 쪼개는지, 그리고 애니메이션의 상태가 무엇에 의존하는지다.

그래프를 나눠라

복잡한 움직임은 하나의 수식으로 표현하기 어렵다. 이럴 때는 그래프를 구간별로 쪼개는 방법을 사용할 수 있다.

예를 들어, 지도 앱에서 핀이 제자리에서 통통 튀다 안착하는 애니메이션을 생각해보자. 물리 시뮬레이션으로도 비슷한 결과를 낼 수 있지만 자연스러운 움직임이 항상 좋은 애니메이션은 아니다. 두 번째 바운스의 높이를 의도적으로 줄이거나, 마지막 안착을 더 부드럽게 만들고 싶을 수 있다. 이럴 때 구간을 나누면 이런 세밀한 조정이 가능해진다. 각 구간은 단순한 그래프 조각이고 이들을 이어 붙이면 전체 애니메이션이 완성된다.

핀 바운스 — 각 구간을 독립적으로 설계한 뒤 이어 붙인다

이러한 그래프는 수학에서 구간별 함수piecewise function라 부르는 것과 같은 원리다. 복잡한 움직임을 단순한 조각으로 나누고 각 조각을 개별적으로 설계한 뒤 다시 이어 붙이는 것이다.

무엇에 의존하는가

그래프의 가로축이 꼭 시간일 필요는 없다. 이 부분을 좀 더 구체적으로 살펴보자. 애니메이션의 값이 무엇에 의존하는가를 파악하는 것은 설계의 출발점이다. 대표적으로 세 가지 유형이 있다.

시간 기반 애니메이션은 가장 흔한 형태다. 시작 시점부터 시간이 흘러감에 따라 값이 변한다. 이전 상태에 시간 변화를 적용해 다음 상태를 만들어내는 것으로, f(state, Δt) → nextState 형태로 표현할 수 있다. 매 프레임마다 이 함수를 반복 적용하면 애니메이션이 진행된다.

값 기반 애니메이션은 시간이 아닌 특정 값이 입력이 된다. 대표적인 예가 스크롤 기반 패럴랙스 효과다. 사용자가 스크롤을 내리는 만큼 요소의 위치나 투명도가 변한다. 스크롤 오프셋이 그래프의 가로축이 되는 셈이다. 마우스 위치, 센서 데이터 등 다양한 값이 가로축이 될 수 있다.

이벤트 기반 애니메이션은 특정 트리거에 의해 값이 전환된다. 호버, 클릭, 데이터 로딩 완료 같은 이벤트가 발생하면 현재 값에서 다음 값으로의 전환 애니메이션이 시작된다. 이 경우 애니메이션은 이벤트에 의해 촉발되고, 전환 과정 자체는 시간 기반으로 동작하는 혼합 형태가 많다.

어떤 애니메이션을 설계할 때 가장 먼저 물어야 할 질문은 "이 애니메이션의 값은 무엇에 의존하는가?"이다. 이 질문에 답하면 그래프의 가로축이 정해지고 거기에 맞는 구현 방식을 선택할 수 있다.

그래프를 쪼개는 법과 값의 의존성을 파악했다면 이제 조각들을 조립할 차례다. 조립 방식에 따라 세 가지 패턴이 있다.

파이프라이닝

가장 직관적인 조립 방법은 조각들을 순서대로 나란히 놓는 것이다. 이를 파이프라이닝이라 한다. 예를 들어, 알림이 나타나는 애니메이션을 설계한다면 다음과 같이 세 조각이 나온다.

  1. 배경 어두워짐 — opacity 0 → 0.5, 200ms
  2. 카드 올라옴 — 위치 아래 → 제자리, 300ms, ease-out
  3. 내용 드러남 — opacity 0 → 1, 200ms

각 조각은 독립적인 그래프지만 시간축 위에 순서대로 배치하면 파이프라인이 된다.

배치 전략은 다양하다. 반드시 이전 조각이 끝난 뒤에 시작할 필요는 없다.

  • 순차: A가 끝나면 B 시작
  • 겹침: A의 80% 지점에서 B 시작. 조각 사이 끊김이 줄어든다
  • 동시: A와 B가 동시에 시작하되, 서로 다른 속성을 변화시킨다
  • 스태거: 같은 애니메이션을 여러 요소에 시간차를 두고 적용한다. 리스트 아이템이 하나씩 나타나는 것이 대표적이다

파이프라이닝의 장점은 각 조각을 독립적으로 수정할 수 있다는 것이다. 카드가 올라오는 속도가 너무 빠르면 그 조각만 수정하면 된다. 전체 애니메이션을 처음부터 다시 설계할 필요가 없다.

새 메시지
안녕하세요! 확인해주세요.
배경
카드
내용
세 조각이 시간축 위에 순서대로 배치된다

상태 전환으로 설계하기

파이프라이닝이 어떠한 축을 기준으로 순서대로 배치하는 것이라면, 상태 전환은 조건에 따라 다음 단계로 넘어가는 방식이다. 하나의 요소가 중간에 성격이 완전히 달라지는 여러 단계를 거칠 때 적합하다.

폭죽 애니메이션을 설계해보자. 하나의 파티클이 다음 상태를 거친다.

상태변하는 것그래프전환 조건
발사높이 ↑가속 (ease-in)속도 = 0 → 폭발
폭발파티클 분리순간 전환즉시 → 확산
확산반경 ↑, 속도 ↓감속 + 중력시간 경과 → 소멸
소멸opacity ↓선형 감소opacity = 0 → 제거

각 상태는 서로 다른 그래프를 가진다. 발사는 시간-높이, 확산은 시간-반경, 소멸은 시간-투명도다. 하나의 수식으로 표현하기는 어렵지만 상태별로 끊으면 각 조각은 단순하다.

핵심은 전환 조건을 명확히 정의하는 것이다. "속도가 0이 되면 폭발", "투명도가 0이 되면 제거"처럼 다음 상태로 넘어가는 트리거를 명시하면 복잡한 애니메이션도 상태 머신처럼 관리할 수 있다. 따라서 다음과 같이 다이어그램으로 표현할 수도 있다.

파이프라이닝과의 차이는 언제가 아니라 어떤 조건에서가 중심이라는 점이다. 시간이 아닌 상태 값에 의해 전환이 결정되므로, 물리 시뮬레이션이나 사용자 인터랙션처럼 결과를 미리 예측하기 어려운 상황에서 자연스럽게 들어맞는다.

발사 → 폭발 → 확산 → 소멸발사

속성 분리

간혹 여러 속성이 동시에 변화해야 할 때도 있다. 이런 경우에는 여러 속성을 독립적인 트랙으로 분리하는 것이 좋다.

요금제 카드를 클릭해서 선택하는 UI를 예로 들어보자. 카드를 클릭하면 선택했음을 알리기 위해 테두리가 파란색으로 바꿀 필요가 있다. 그리고 강조를 위해 카드를 살짝 커지게 만들 필요도 있다. 더불어 사용자가 관심을 가지는 상세 내용이 드러나도록 묘사할 필요도 있다. 세 가지 변화가 동시에 일어나지만 각각 필요로 하는 움직임의 성격은 다를 것이다.

이들을 하나의 수식으로 묶으면 각각의 요구를 충족할 수 없다. 대신 각 속성을 독립적인 트랙으로 분리하면 된다. 다음 예제를 확인해보자.

Pro 플랜
월 29,000원
무제한 프로젝트
팀 협업 기능
우선 지원
테두리
150ms, ease-out
크기
스프링
상세
400ms, ease-out
카드를 클릭하세요 — 세 속성이 각자의 곡선으로 독립 변화한다

속성 분리의 핵심은 각 트랙이 서로를 몰라도 된다는 것이다. 테두리 트랙을 수정해도 크기나 상세 내용에 영향이 없다. 이 독립성 덕분에 하나의 속성만 미세 조정하거나 새 속성을 추가하는 것이 쉬워진다.

랜덤성

앞서 상태 전환 부분에서 폭죽 예제를 살펴보았다. 만약 모든 파티클이 정확히 같은 속도, 같은 각도 간격으로 퍼진다면 어떨까? 기하학적으로는 정확하지만 자연스럽지는 않다. 현실의 폭죽은 조금씩 불규칙하다. 이 불규칙함이 오히려 자연스러움을 만든다.

애니메이션에 랜덤성을 더하면 기계적인 느낌을 벗어날 수 있다. 파티클의 속도에 ±20% 편차를 주거나, 각도에 약간의 흔들림을 추가하거나, 시작 타이밍을 미세하게 어긋나게 하는 것이다.

하지만 여기서 중요한 원칙이 있다. 랜덤은 진짜 랜덤이면 안 된다. 완전한 무작위는 예측 불가능하고 의도하지 않은 결과를 만든다. 예를 들어, 파티클이 전부 한쪽으로 쏠리거나 크기가 너무 극단적으로 나올 수 있다.

순수 랜덤
속도 0 ~ 5
크기 1 ~ 9
색상 0° ~ 360°
분포 무작위
통제된 랜덤
속도 2 ~ 3.5
크기 2.5 ~ 4
색상 기준 hue ± 20°
분포 균등 배치 + 미세 편차

애니메이션을 의도한 것의도하지 않은 것으로 나눠 생각하면 도움이 된다. 파티클이 위로 올라가는 것은 의도한 움직임이고, 각 파티클의 미세한 속도 차이는 의도적으로 넣은 비의도적 요소다. 설계자는 "어디까지를 통제하고, 어디부터를 랜덤에 맡길 것인가"를 결정해야 한다. 이 경계를 잘 설정하면 질서와 자연스러움이 공존하는 애니메이션을 만들 수 있다.

양방향성 고려

지금까지 살펴본 애니메이션은 대부분 한 방향으로 흘렀다. 시작에서 끝으로, 0에서 1로. 하지만 스크롤에 반응하는 애니메이션이나 드래그 인터랙션처럼 사용자가 진행 방향을 바꿀 수 있는 경우가 있다. 스크롤을 내리면 요소가 나타나고, 다시 올리면 사라진다. 이런 애니메이션은 설계할 때 역재생을 고려해야 한다.

다음 예제를 살펴보면 스크롤에 반응하는 패럴랙스 효과가 어떻게 양방향으로 설계되는지 볼 수 있다. 요소가 스크롤에 따라 나타나고 사라지는 애니메이션이지만, 스크롤 방향이 바뀌어도 자연스럽게 이어진다.

↓ 스크롤
새로운 알림
스크롤 위치가 곧 애니메이션의 진행도가 된다. 올리면 자연스럽게 되돌아간다.
↑ 다시 올려보세요
0%
스크롤 위치가 곧 진행도 — 올리면 자연스럽게 되돌아간다

패럴랙스와 같은 값 기반 애니메이션은 양방향성을 고려하기가 상대적으로 쉽다. 그래프의 가로축이 스크롤 오프셋이기 때문에, 방향이 바뀌어도 같은 그래프를 역방향으로 따라가면 된다. 하지만 이벤트 기반 애니메이션처럼 버튼 클릭으로 나타나고 사라지는 경우는 좀 더 신경 써야 한다. 양방향성을 고려하지 않으면 다음과 같은 문제가 생긴다.

  • 점프: 나타나는 도중에 숨기기를 누르면, 현재 위치를 무시하고 처음부터 사라지는 애니메이션이 시작된다. 위치가 순간이동하면서 뚝 끊긴다
  • 동일한 그래프: 나타날 때와 사라질 때 같은 이징 그래프를 사용하면 움직임이 어색해진다. 예를 들어, ease-in으로 나타났다면 사라질 때는 ease-out을 써야 자연스럽다.

양방향성을 제대로 고려하면 방향이 바뀌는 순간 현재 상태에서 이어서 반전된다. 아래 예제에서 나타나는 도중에 빠르게 토글해보면 차이를 느낄 수 있다.

고려하지 않은 경우
고려한 경우
나타나는 도중에 빠르게 토글해보세요

정말 복잡한 애니메이션은?

지금까지 다룬 기법으로 꽤 많은 애니메이션을 설계할 수 있지만 한계는 분명히 존재한다. 캐릭터가 걷고 뛰는 동작, 손으로 그린 듯한 모핑, 수십 개의 레이어가 정교하게 맞물리는 인트로와 같은 애니메이션을 코드만으로 표현하는 것은 현실적이지 않다.

이런 경우에는 전문 도구를 사용하는 것이 맞다. After Effects로 만든 애니메이션을 Lottie로 내보내거나, Rive처럼 인터랙티브 애니메이션에 특화된 도구를 쓰는 방식이다. 혹은 아예 영상으로 제작해서 재생하는 것도 방법이다. 코드로 모든 것을 해결하려는 것보다 적절한 도구를 선택하는 것이 더 나은 결과를 만든다.

Lottie를 사용한 복잡한 애니메이션

반대로 말하면 코드로 작성하는 애니메이션의 강점은 실시간 인터랙션에 있다. 사용자의 입력에 즉각 반응하고 상태에 따라 동적으로 변하는 움직임은 미리 만들어 둔 영상으로는 구현하기 어렵다. 이 글에서 다룬 기법들이 빛을 발하는 지점이 바로 여기다.

마치며

애니메이션을 구현할 때 막막했던 경험이 있다면, 이 글이 "어디서부터 시작할지"를 찾는 데 도움이 되었기를 바란다. 결국 애니메이션 설계의 핵심은 분해다. 어떤 움직임이든 쪼개면 단순해지고 단순한 조각은 그래프로 그릴 수 있다. 이 과정을 의식적으로 연습하면 머릿속의 움직임을 코드로 옮기는 거리가 점점 가까워질 것이다.