여러 인터랙션 라이브러리(gsap, framer motion, animeJS + Lottie)를 분석해볼 것이다. 모두 할 수 있을 지는 모르겠다.
화려한 것에 잘 매료되는 사람에게 웹 인터랙션은 너무 매력적이다. 나도 항상 내부 로직에 대한 깊은 탐구를 지향하면서도 겉으로 보여지는 것에 매번 매료되어 추상화의 벽을 넘나들었다.
학창시절부터 지금까지 영상과 모션그래픽은 얕게나마 내 주위에 있었다. 그래서 영상 쪽 도메인에 어느정도 익숙하다.
그래서 난 이걸(인터랙션) 파해쳐 봐야겠다. 나의 사명같은 느낌...
바로 직전 프로젝트 (velog에 언젠가는 정리해야지...)에서도 타임라인을 구현해 영상 편집 데이터 추출 후 ffmpeg로 미디어 처리하는 것을 했기에 타이밍상 감이 살이 있는 지금이 좋을 것 같다고 생각한다. 이거 하고 싶어서 도저히 이력서 작성할 정신이 없다...(이것이 ADHD dev...<- gpt가 실제로 나에게 한 말이다.)
인터랙션(Interaction)은 '원인과 결과'의 관계, 즉 사용자와 시스템 간의 상호작용 전체를 의미한다. 반면 애니메이션(Animation)은 그 상호작용의 결과로 나타나는 '시간에 따른 시각적 변화'를 의미한다.
어쩌면 나는 애니메이션을 분석하는 것 일 지도 모르겠다. 그렇지만 일단 해보자. 지금부터 애니메이션에서 사용되는 키워드(옵션)에 대해서 알아보자.
본격적인 키워드 분석에 앞서, 우리는 모든 애니메이션의 가장 근본적인 원리인 Tween 을 먼저 이해해보자.
트윈은 '인-비트윈(In-between)'에서 유래한 말로, 시작 값과 끝 값 사이의 중간 값들을 끊임없이 계산하고 생성하는 과정 그 자체를 의미한다.
말이 어렵다...
예를 들어보자. 나는 원피스라는 애니메이션을 좋아했다. 원피스의 원작자는 "오다 에이치로"이다. 오다는 영상이 아닌 정적인 만화로 작품을 그려낸다.
이걸 애니메이션화 한다면 오다가 그린 늘어나기 전의 루피의 팔과 고무고무 열매에 의해 늘어난 루피의 팔 이라는 두개의 시작과 끝상태가 있을 것이다. 그럼 애니메이터들은 이 두가지 상태 사이의 움직임에 해당하는 모든 부분들을 그려 낸다.
원피스가 24프레임이라면, 팔이 1초만에 늘어났다고 가정했을 때 1과 24사이의 22개의 프레임을 그려내는 것을 '트위닝' 이라고 한다.
(이해를 돕기위한 비유니 실제 원피스만화의 제작과는 조금 다를 것이다. 오다 에이치로 대신 수석 애니메이터를 대입하는 것이 좀더 맞는 표현일 수 있다.)
중간 값을 그려내는 것이 트위닝
조금 더 수치적으로 설명하자면, 한 요소의 위치를 X축 0px에서 100px로 옮긴다면, 트위닝은 그 사이의 1px, 2px, ..., 99px에 해당하는 모든 중간 상태를 만들어내는 작업이다.
애니메이션 라이브러리의 가장 본질적인 역할이 바로 이 '트위닝'을 빠르고 효율적으로 수행하는 것이다.
보통의 라이브러리는 60프레임, 그러니까 1초에 60번 값을 계산하며 화면에 그리는 것을 자연스럽게 보여지도록 하는 것을 목표로한다.
이제부터 살펴볼 Properties, Duration, Easing 같은 키워드들은 모두 이 '트위닝' 과정을 어떻게 제어할 것인지에 대한 구체적인 방법들이다.
모든 애니메이션은 단 3가지 요소만 있다면 어떤 움직임이든 정의 할 수 있다.
아래에 나올 용어들은 대부분의 인터랙션 라이브러리에서 통용되는 용어들이다.
"무엇을(Target)" 과 어떻게(Properties) 에 해당한다. 이 둘은 많은 부분이 엮여 있기 때문에 각각보다는 두개를 동시에 소개하는 편이 좋을 것 같다. 대부분의 라이브러리는 DOM 요소를 Target으로 지정하는데, 이는 웹이 본질적으로 Document Object Model 위에 구축된 인터페이스이기 때문이다. 또한 Properties는 위치를 바꾸는 translateX, 크기를 키우는 scale, 서서히 사라지게 하는 opacity처럼, 대상의 상태 변화를 구체적으로 명시하는 역할을 한다.
기술적으로 라이브러리는 DOM 요소 자체보다, 해당 요소의 style 객체에 있는 속성 값을 변경한다. 하지만 뛰어난 라이브러리들은 여기서 더 나아가, JavaScript 객체의 모든 숫자 타입 값을 Target으로 삼을 수 있다.
여기서 말한 뛰어난 라이브러리와 숫자 타입이 조금 추상적으로 와닿을 수 있다.
뛰어난 라이브러리라고 하면, 현업에서 사용되는 상용 라이브러리, 즉 내가 분석하려고 하는 GSAP, Framer Motion, AnimeJS 등 을 말한다. 이러한 라이브러리들은 JavaScript 객체의 숫자 값에 애니메이션을 주는 뛰어난 능력을 갖추고 있다.
숫자 타입, 숫자 값은 값이 숫자로 이루어져 있어 수학적인 계산이 가능한 모든 속성을 의미한다.
앞서 말했듯이 애니메이션은 트위닝이라는 시작 값과 끝 값 사이의 중간 값을 계산하는 과정이 중요하다. 라이브러리는 내부적으로 현재값 = 시작값 + (끝값 - 시작값) * 진행률
같은 공식을 매 프레임마다 실행한다.
그럼 애니메이션을 줄 수 있는 숫자 타입의 예시들을 보자.
DOM요소 자체보다 style속성을 건드린다고 앞서 말했다.
css의 opacity
는 단순히 0과 1사이의 숫자 그대로를 value로 가진다.
또한 transform: translateX(100)
이나 transform: scale(1.5)
와 같이 괄호 안의 값을 사용하는 방식 역시 사용 가능하다. width
,height
처럼 px이나 %같은 단위가 붙는 숫자 값역시 단위는 그대로 사용하고 그 앞에 붙은 숫자만 변화하는 형식으로 사용된다.
반면, display
, 나 positon
같이 위치나 상태가 변화하는 속성이어도 중간값이 존재하지 않는다면 애니메이션의 대상이 되기는 어렵다.
위에서 말했듯이 JavaScript 객체의 값역시 애니메이션의 대상이 된다.
객체: { x: 50, score: 0 }
에서 x와 score의 값을 변화시키는 것을 예시로 준비했다.
다시 한 번 말하지만 JavaScript 객체의 모든 숫자 타입 값을 Target으로 삼을 수 있다.
이것이 중요한 이유는 Canvas나 WebGL 같은 환경 때문이다.
캔버스는 그 자체로 하나의 DOM요소이다. 그렇기에 캔버스 위 도형은 DOM요소가 될 수 없으며 이러 인해 직접 타겟팅 할 수도 없다. 대신 그 도형의 좌표나 크기가 담긴 JavaScript객체 자체를 타겟으로 삼고, 라이브러리가 그 숫자를 변경하면 redraw
방식으로 애니메이션이 구현된다. canvas는 하나의 객체라는 생각을 계속 가지고 있으면 편하다. canvas는 하나의 스크린이다. redraw
란 이 canvas라는 하나의 스크린이 매 프레임마다 전체적으로 다시 그려지는 방식이다. 이것이 canvas의 동작 방식이다.
SVG는 XML기반의 마크업 언어이다.
디자인을 조금이라도 접한 사람들은 벡터와 비트맵 그래픽의 차이를 알 것이다. 혹은 개발자여도 jpg나 png를 사용했을 때는 확대했을 때 이미지가 흐릿하게 보이는데 svg는 아무리 확대를 하더라도 이미지가 손상되지 않는 것을 경험해 보았을 것이다.
여기서 근본적인 두 방식의 차이점이 나온다.
(이번에는 적절한 자료가 많길래 서칭해서 찾아왔다.귀차니즘...)
비트맵 방식은 하나의 픽셀(점)에 어떠한 색상(rgba)데이터가 온다고 명시된다. 점묘화를 생각해봐도 좋다. 혹은 디스플레이의 표현방식과 비슷하다.
반면에 벡터는 좌표에 존재하는 어떠한 점들을 이어서 만들어진 도형의 정보로 표현하는 방식이다. 각 좌표와 그 사이의 데이터만 있기에 아무리 확대한다고 해도 이미지가 흐릿하게 보일일이 없다.
아마도 직접 SVG코드를 읽어보지 않은 일부는 SVG가 XML기반이라는 것에 놀랄 수도 있다.
adobe의 illustrator 같은 벡터 그래픽 툴로 export했었던 SVG가 결국 읽을 수 있는 코드였다는 것에 친근감이 느껴질 수도 있다. 하지만 실제 코드를 보면 읽을 수 없다는 것을 깨달을 것이다.
보통 svg는 html 태그 수준에서 하나의 객체로 사용된다. 크롬 개발자 도구를 키고 선택해보면 알 수 있다. 하지만 DOM 레벨에서는 circle
, rect
, path
등 은 모두 DOM의 요소로 분해돼서 해석된다.
그렇다면 QuerySelector 등으로 css속성을 건드리면 되는 건가??
물론 가능하고 올바른 방법이다. 하지만 svg는 css속성외에도 고유 속성을 가지고 있다. 그래서 다른 돔 요소에 비하여 훨씬 다채로운 제어가 가능하다.
<svg width="100" height="100">
<circle cx="50" cy="50" r="20" fill="blue" />
</svg>
위의 코드와 같이 원의 반지름 r, 중심 좌표 cx, cy 등의 요소도 직접 접근하여 애니메이션을 부여할 수 있다.
애니메이션이 시작부터 끝까지 지속되는 시간이다. 이 값이 짧을수록 움직임은 빨라지고, 길수록 느려진다. 애니메이션의 전체적인 속도감을 결정하는 핵심 요소이다.
모션그래픽을 조금이라도 접해본 사람은 알 것이다. 키 프레임 사이의 움직임이 일정하면 얼마나 기계적으로 보이는 지에 대해서 말이다. 이에 익숙하지 않은 사람은 가속도를 생각해보자. 가속도 없이 일정하게 떨어지는 사과가 얼마나 어색할까... 표현이 조금 이상한 것 같으니 시각 자료를 보자.
이제 조금 감이 올 것이다.
조금 다른 것을 보자
위의 두원은 동일한 시간동안 동일한 거리를 움직이지만 각 순간의 속도가 다르다. 이것이 Easing이다.
easing은 움직임의 가속도 곡선을 의미한다. 같은 시간을 움직이더라도, 어느순간에는 빨랐다가 느려지게 하거나,공처럼 통 통 튀는 것 처럼 '역동성' 을 부여한다.
보통 기본 값은 처음 봤던 일정한 속도로 기계처럼 움직이는 linear(등속)이다.
다양한 수식을 통해서 어떠한 순간(해당하는 프레임)에 어느 값을 가져야 하는 지 계산해서 적용할 수 있다.
Ease-In(점점 빠르게), Ease-Out(점점 느리게), Ease-In-Out(천천히 시작해서, 중간에 빨라졌다가, 부드럽게 멈추게) 등의 움직임과 그 수치 등을 제어 할 수 있다.
물론 그 외에 특정 느낌들을 극대화하기위한 다양한 Easing들이 있다.
모션그래픽을 접해봤으면 익숙할 텐데, 다른 사람 입장에서 어떻게 느껴질 지는 모르겠다.
Tween 사이, 어느 한 시점마다 값을 명시한다는 개념으로 설명하면 좋으려나...?
다시말해 Tweening이 시작과 끝 사이를 채우는 것이라면, Keyframe은 그 과정에 여러 개의 중간 경유지를 설정하는 방식이라고 말할 수 있겠다.
예를 들어, translateX를 0 -> 100
으로 한 번에 가는 대신, 0 -> 200 -> 50 -> 100
과 같이 여러 지점을 거쳐 가는 복잡한 경로를 하나의 속성에 정의하는 것이다.
이건 길게 설명 안해도 될 것이다. 애니메이션이 즉시 시작하는 것이 아닌 시작 후 얼마나 대기한 다음에 효과가 실행 될지를 설정하는 것이다.
하나의 씬(혹은 입력)에 하나의 애니메이션만 존재하진 않는다. 그래서 단순한 움직임을 넘어 복잡한 시퀀스를 만들려면, 애니메이션의 '흐름'을 제어하는 개념이 필요하다.
여러 애니메이션을 하나의 시간 흐름 위에서 관리하는 기능이다.
첫 번째 애니메이션이 끝나면 두 번째를 시작하고, 동시에 세 번째와 네 번째를 함께 실행하는 식의 복잡한 시나리오를 짤 수 있다.
영상편집의 UI를 개념적으로 생각하면 좋지 않을까 싶다.
그럼 단순히 Delay
를 사용해서 구현하면 되지않을까라는 의문을 가질 수도 있다.
|-----| 애니메이션1 (delay: 0, duration: 5)
|-----| 애니메이션2 (delay: 5, duration: 5)
위와 같은 방법으로 말이다.
그치만 개발자라면 애니메이션1의 duration
을 수정 할 때 굳이 애니메이션2의 delay
를 수정하는 수고를 하고싶진 않을 것이다.
타임라인을 사용하면 상대적으로 시간을 관리할 수 있다. 각 애니메이션의 시작 시간을 절대적인 숫자를 사용하지 않고 다른 애니메이션을 기준으로 상대적으로 배치할 수 있다.
예를 들어 "첫 번째 애니메이션이 끝나는 시점에 바로 시작", "두 번째 애니메이션보다 0.5초 늦게 시작" 같은 로직으로 가독성 좋고 유지보수도 쉽게 구현할 수 있다.
[타임라인]
Track 1: |===애니메이션 1===|
Track 2: |---애니메이션 2---|---애니메이션 3---|
시각적으로 표현하면 이렇다. 실행 시점을 절대값으로 명시적할 필요가 없다.
또한 하나의 타임라인을 하나의 단위로 제어할 수 있게 된다. 그럼 타임라인 단위로 한번에 재생, 일시정지, 역재생, 특정 시간으로 이동, 재생속도 조절 등의 옵션을 사용할 수 있다. 마치 여러 애니메이션이 모인 타임라인을 하나의 영상처럼 취급하는 것이다.
Stagger는 여러 Target 요소에 순서대로 시간 차를 두어 애니메이션을 적용하는 기술이다. 모든 요소가 동시에 움직이는 것이 아니라, 마치 도미노처럼 리드미컬한 효과를 손쉽게 만들 수 있다. 메뉴 항목이 순서대로 나타나거나, 여러 개의 카드가 차례로 날아 들어오는 효과를 구현할 때 유용하게 사용된다.
Timeline이 여러 다른 애니메이션을 제어할 때 사용한다면 Stagger는 하나의 애니메이션을 여러 대상에게 순서대로 적용할 때 사용한고 생각하면 된다.
애니메이션의 생명주기 콜백은 단순히 움직임의 시작과 끝을 알리는 것을 넘어, 애니메이션이라는 비동기작업을 애플리케이션의 상태 관리 및 로직 흐름에 통합시키는 핵심적인 역할을 한다.
조금 쉽게 말해보자면 애니메이션이 없을 때는 보통 입력과 이벤트 사이에 어떠한 틈이 없이 즉시 시전 된다. 하지만 애니메이션이 들어가면 애니메이션 실행시간 동안의 틈이 생긴 후 상태가 업데이트 되어야 한다.
예를 들어보자. 간단한 모달창을 여는 버튼이 있다고 가정하자. 애니메이션이 없다면 해당 버튼을 누르면 "isModalOpen"이라는 상태가 즉시 true로 갱신 될 것이다. 하지만 1초 동안의 모달이 보여지는 애니메이션이 있다면 "isModalOpen"의 상태가 true로 갱신 되는 것은 애니메이션이 끝났다는 콜백을 받은 후여야 할 것이다.
아래의 두 시각자료에서 isModalOpen의 상태가 갱신되는 시점을 중심으로 보자.
1. 애니메이션 없이 즉시 실행
2. 애니메이션이 끝난 후 콜백을 받고 상태 갱신(우리가 원하는 방식)
gif변환과정을 통하니 조금 매끄럽지 않게 보여지는데 실제로는 아주 매끄럽게 움직였다...상용 라이브러리를 사용한거니까...
마지막으로, 왜 transform과 opacity 속성이 애니메이션에서 중요하게 다뤄지는지 알아야 한다.
우리 눈에 보이는 화면은 브라우저가 Layout → Paint → Composite 단계를 거쳐 그린다.
아래는 각 단계에서 무엇을 하는지에 대해서다.
Layout: 각 요소의 크기와 위치를 계산 (width, height, margin 변경 시 발생)
Paint: 각 요소를 색상과 이미지 등 픽셀로 채우기 (background-color, box-shadow 변경 시 발생)
Composite: 계산된 여러 레이어를 순서대로 쌓아 최종 화면을 완성.
여기서 중요한 점은, 대부분의 CSS 속성을 변경하면 Layout이나 Paint 단계부터 다시 실행되어 연산 부담이 크다. 이를 각각 Reflow와 Repaint라고 부르며, 애니메이션이 버벅거리는 주된 원인이 된다.
하지만 transform
과 opacity
는 다르다. 이 속성들은 별도의 합성 레이어(Composite Layer)에서 독립적으로 처리되므로, 비용이 큰 Layout과 Paint 단계를 건너뛰고 마지막 Composite 단계만 다시 실행한다. 이 과정은 GPU에 의해 직접 처리되기 때문에 훨씬 빠르고 부드러운 애니메이션이 가능해진다.
애니메이션의 성능을 생각한다면 "이 움직임을 transform과 opacity만으로 구현할 수 있는가?" 라는 질문을 주기적으로 던질 필요가 있지 않을까??
아름답습니다.