Javascript Effective Animation

JavaScript 효과적인 애니메이션 구현하기

애니메이션은 하나 이상의 이미지(frame)을 일정한 시간(second) 간격으로 순차적으로 호출해 이미지가 움직이는 것처럼 보이도록 착시 현상을 유도하는 기술이다. 이때 보이는 이미지의 개수와 시간 간격의 비를 FPS(frames per cecond)라고 하며, FPS 값이 클수록 더 자연스러운 애니메이션을 표현할 수 있다.

보통 눈의 잔상효과를 이용해 움직이는 동작을 표현할 때는 2530FPS 정도를 사용하면 애니메이션 효과를 느끼게 할 수 있다. 하지만 더 자연스러운 애니메이션을 표현하려면 60120FPS를 유지해야 한다. 웹에서는 60FPS 정도면 충분히 자연스러운 효과를 낼 수 있다.

자바스크립트 기반의 애니메이션

웹 기술의 발전과 더불어 현재는 자바스크립트 외의 다양한 방법으로 애니메이션을 구현할 수 있다. 하지만 웹이 생겼던 초기에는 애니메이션을 구현하는 방법이 많지 않았다. 그 가운데 가장 전통적이고 전형적인 방법은 setTimeout() 메서드와 setInterval() 메서드를 이용한 방법이다.

자바스크립트 기반의 애니메이션 구현 방법은 ‘애니메이션 동작을 처리하는 함수’를 일정 시간(ms)마다 호출해 ‘애니메이션 대상(DOM)’을 이동하거나 상태를 변경해서 애니메이션을 구현하는 방법이다.

구현하기

자바스크립트 기반의 애니메이션은 대개 다음과 같은 순서로 구현한다.

1.애니메이션 대상이 되는 DOM을 작성한다. 다음 예제에서는 100 x 100px 크기의 빨간색 사각형 영역을 마크업으로 구성했다.

1
<div id="box" style="position:absolute;left:0;bordr:1px solid red;width:100px;height:100px"></div>

2.원하는 FPS에 해당하는 단위 시간(ms)을 구하고 단위 시간마다 애니메이션을 처리하는 함수를 호출한다. 단위시간은 다음과 같은 공식으로 구할 수 있다.

1
2
3
단위 시간(ms) = 1000 / 원하는 FPS

예를 들어, 60FPS라면 '1000/60 = 약 16.7ms'의 단위 시간을 구할 수 있다. 즉, 16.7ms마다 이미지를 하나씩 보여주거나 상태를 변경하면 60FPS의 애니메이션을 표현할 수 있다.

3.애니메이션 대상이 될 DOM 요소의 상태를 변경하는 함수를 작성한다. 이 예제에서는 fAnimate() 함수로 작성했다.

4.setTimeout() 메서드 또한 setInterval() 메서드를 이용해 fAnimate() 함수를 단위 시간(ms) 마다 호출한다.
다음은 setTimeout() 메서드를 이용해 아이디가 ‘box’인 DIV 요소를 1000/60초(약 16.7ms)마다 5px씩 오른쪽으로 300px 이동하는 예다.

1
2
3
4
5
6
7
8
9
10
11
12
var elBox = document.getElementById('box'),
nAnimateTimer = null,
fAnimate = function() {
elBox.style.left = parseInt(elBox.style.left, 10) + 5 + 'px';

//종료시점
if (parseInt(elBox.style.left, 10) < 300) {
nAnimateTimer = setTimeout(fAnimate, 1000/60);
}
};

nAnimateTimer = setTimeout(fAnimate, 1000/60);

다음은 setInterval() 메서드를 사용해 아이디가 ‘box’인 DIV 요소를 1000/60초(약 16.7ms)마다 5px씩 오른쪽으로 300px 만큼 이동하는 예다.

1
2
3
4
5
6
7
8
9
10
11
12
var elBox = document.getElementById('box'),
nAnimateinterval = null,
fAnimate = function() {
elBox.style.left = parseInt(elBox.style.left, 10) + 5 + 'px';

//종료시점
if (parseInt(elBox.style.left, 10) < 300) {
clearInterval(nAnimateinterval);
}
};

nAnimateinterval = setInterval(fAnimate, 1000/60);

setTimeout() 메서드는 함수를 한 번만 호출하고, setInterval() 메서드는 함수를 반복해서 호출한다는 차이가 있다. 하지만 종료 시점이 있고, 단위 시간마다 반복되는 구조라는 점에서 기본적인 구조는 동일하다.

자바스크립트 기반 애니메이션의 문제점

setTimeout() 메서드나 setInterval() 메서드로 애니메이션을 구현할 수 있지만, 이방법에는 두 가지 치명적인 문제점이 있다.
setTimeout() 메서드와 setInterval() 메서드를 사용할 때마다 브라우저는 애니메이션 대상인 DOM 요소를 변경하고, 이로 인해 리렌더링(re-rendering)이 단위 시간마다 발생한다. 과도한 리렌더링은 CPU의 점유율을 높여 모바일 기기의 전력이 빠르게 소모된다. 또한 브라우저가 백그라운드에서 실행되고 있을때, 최소화됐을 때, 화면에 보이지 않을 때 등 모든 상황에서 애니메이션이 계속 호출되어 전력이 소모된다.

자바스크립트 엔진은 단일 스레드 기반으로 동작하며 비동기로 발생하느 이벤트를 큐(queue)에 쌓아 순차적으로 처리한다. 그래서 setTimeout() 메서드와 setInterval() 메서드에서 사용한 시잔의 정확성을 보장하지 못한다. 이로 인해 실제 애니메이션은 매끄럽지 못하게 표현되거나 부자연스럽게 표현된다.
setTimeout() 메서드나 setInterval() 메서드를 이용하더라도 앞에서 언급한 문제점을 모두 완벽하게 해결 할 수 없다. 하지만 그 영향을 최소화 할 수는 있다.

첫 번째 방법은 DOM 기반의 스타일 변경을 최소화해 리렌더링을 최소화하는 방법이다. 다음과 같이 구현하면 스타일 변경을 줄일 수 있다.
애니메이션 대상의 위치 속성을 ‘position:absolute’로 지정하면 이러한 대상 요소의 위치를 변경할 때 다른 요소의 미치는 영향을 줄일 수 있다.

대상 요소의 투명도와 배경 이미지의 변경을 자제한다. 배경 이미지 대신 고정된 크기의 IMG 요소로 대체 하루 있으면 좋다.
두 번째 방법은 setTimeout() 메서드 대신 requestAnimationFrame API를 사용하는 방법이다.

requestAnimationFrame API와 setTimeout() 메서드는 기능이 거의 같다. 하지만 setTimeout() 메서드가 단위 시간마다 무조건 애니메이션을 호출하는 것과 달리, requestAnimationFrame API는 브라우저에서 화면 업데이트가 가능한 시기를 통보받아 애니메이션을 호출한다. 그렇기 때문에 끊김 없는 부드러운 애니메이션을 구현할 수 있으며, 불필요한 자원 사용을 방지해 전력소비를 줄일 수 있다.

다음은 requestAnimationFrame API를 사용해 애니메이션을 구현한 예다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
window.requestAnimationFrame = (function() {
return window.requestAnimationFrame || window.webktRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function() {
window.setTimeout(callback, 1000/60);
}
})();

var elBox = document.getElementById('box'),
nAnimateTimer = null,
fAnimate = function() {
elBox.style.left = parseInt(elBox.style.left, 10) + 5 + 'px';

//종료시점
if (parseInt(elBox.style.left, 10) < 300) {
nAnimateTimer = requestAnimationFrame (fAnimate);
}
};

nAnimateTimer = requestAnimationFrame (fAnimate);

requestAnimationFrame API는 W3C의 명세 초안(draft)으로 애니메이션을 위한 효과적인 그래픽 타이머다. 하지만 안타깝게도 현재는 크롬 10 이상, 인터넷 익스 플로러 10 이상, 오페라 모바일, 파이어폭스4 이상에서 고유의 접두어(prefix)를 붙여야 사용할 수 있고, 모바일 웹 브라우저에서는 이 메서드가 동작하지 않는다. 그러나 머지않아 모든 모바일 브라우저에서 사용 할 수 있을 것이다.

CSS3 기반의 애니메이션

CSS3 기반의 애니메이션은 자바스크립트 기반의 애니메이션과 달리 애니메이션의 처음과 끝은 개발자가 아닌 브라우저가 인지하고 관리한다. 따라서 다음과 같은 장점이 있다.

웹 브라우저는 단일 스레드를 사용하기 때문에 애니메이션이 동작하는 도중에 비동기식 작업이 진행되면 애니메이션이 매끄럽게 진행되지 못하지만 CSS3 기반의 애니메이션을 이용할 때는 이러한 간섭이 발생하지 않는다.
하드웨어인 GPU의 가속 기능을 사용할 수 있어서 속도가 빠르다.
복잡한 계산으로 얻을 수 있는 애니메이션 이동 곡선을 속성값 설정만으로 간단하게 구현할 수 있다.
CSS3에서는 동적인 애니메이션 처리를 쉽게 적용할 수 있는 transition 속성과 transform 속성, 직관적인 @keyframe 애니메이션등을 제공하고 있다.

구현하기

CSS3 트랜지션은 CSS 속성을 변경함으로써 대상 요소를 일정 시간 동안 자연스럽게 이동하는 기술이다. CSS만을 이용해 구현할 수도 있지만 자바스크립트를 함께 사용하면 더욱 효과적으로 애니메이션을 구현할 수 있다.

CSS3 트랜지션을 이용한 애니메이션은 보통 다음과 같은 순서로 구현한다.

1.애니메이션 대상이 되는 DOM의 처음 스타일을 지정한다.

1
<div id="box" style="position:absolute;border:1px solid red;left:0px;width:100px;height:100px;"></div>

2.애니메이션 대상 요소에 트랜지션을 지정한다.

1
2
3
4
5
6
7
8
9
10
// -webkit-transition : [변경될 대상의 스타일] [duration] [timing function] [delay]
// 애니메이션을 적용할 스타일과 애니메이션 지속 시간(duration), 애니메이션 방식(timing function), 초기 지연(delay)을 지정할 수 있다. 다음은 애니메이션이 천천히 끝나게 하는 ease-out 방식으로 2초동안 left 속성을 변경하는 예다.

<style>
.leftTransition{-webkit-transition:left 2s ease-out}
</style>
<script>
var ele = document.getElementById('box');
ele.className = 'leftTransition';
</script>

3.자바스크립트로 마지막 상태의 스타일을 지정한다.

4.애니메이션이 완료되면 onTransitionEnd 이벤트가 발생한다. 다은은 left 속성을 300px로 지정하는 예다.

1
2
var ele = document.getElementById('box');
ele.style.left= '300px';

transform 속성을 이용한 애니메이션

transform속성을 이용하면 left 속성과 top 속성을 변경하지 않고, translate 값을 변경해 애니메이션을 구현할 수 있다.

이 방법은 다음과 같은 장점이 있다.

레이어(layer)로 구성되어 애니메이션이 발생하는 동안에 레이어 병합을 사용하기 때문에 애니메이션 도중 렌더링이 계속 발생하지 않아 속도가 빠르다.
traslate3d 속성을 사용해 GPU 가속 기능을 사용할 수 있다. 단 안드로이드는 3.0(허니콤) 이상에서 지원한다.
transform 속성과 translate 속성을 이용하려면 다음과 같이 transition 속성에서 변경될 대상의 스타일을 transform으로 지정하고, translate 속성을 사용한다.

1
2
3
<style>
.leftTransition{-webkit-transition:left 2s ease-out;-webkit-transform:translate(0,0)}
</style>

이 때 GPU 기능을 사용하려면 translate 대신 translate3d로 지정하고, translate 속성에 z좌표를 하나 더 지정한다.

1
2
3
<style>
.leftTransition{-webkit-transition:left 2s ease-out;-webkit-transform:translate3d(0,0,0)}
</style>

CSS3 기반 애니메이션의 문제점

웹킷 기반의 모바일 브라우저는 CSS3 트랜지션을 모두 활용해 최적의 애니메이션을 구현할 수 있다. 하지만 다음과 같은 문제점이 있다.

CSS3 사용해서 얻을 수 있는 최고 효과인 성능 향상을 얻으려면 3D 속성을 사용해 GPU 가속을 활성화해야 하지만 3D 속성에 관련된 오류가 여럿 있어 현실적으로 트랜지션을 적용하는 데 많은 어려움이 있다.
GPU 가속 기능을 사용하면 네이티브 앱과 같은 성능을 얻을 수 있는 반면, 전력 소모는 증가한다.
애플의 모바일 기기는 안정적인 GPU를 탑재해 최고의 성능을 얻을 수 있다. 하지만 그 외 제조사의 기기에는 GPU가 있다고 해도 제조사에 따라 GPU의 성능에 확연한 차이가 있다.
기술적으로는 웹에서 애니메이션을 구현하는 최고의 방법은 CSS3를 이용하는 것이다. CSS3를 이용하면 전력 소모 문제를 제외하고는 모든 면에서 다른 방법보다 우수하다. 하지만 실제로는 CSS3를 구현한 모바일 브라우저 및 제조사의 오류로 인해 이 방법을 사용하기는 힘들다. 그러나 브라우저 오류를 우회할 수 있는 여러 방법으로 CSS3와 관련되 오류를 피할 수 있다.

iOS의 브라우저에서는 ‘애니메이션 대상’에 CSS3 Transform 3D(translate3d, preserve-3d) 속성을 처음에 지정할 때 해당 요소가 깜빡이는 문제가 있다. iOS는 CSS3 Transform 3D 속성을 지정할 때 애니메이션에 필요한 정보를 OpenGL Textrue 형태로 GPU에 저장한다. 이때 요소가 깜빡이는 현상이 나타난다. 이 문제를 해결할려면 애니메이션 대상이 되는 요소를 보이지 않게 하고 CSS3를 설정한 다음 요소를 보이게 한다.

그 외에도 iOS 브라우저에서는 화면 전체가 깜빡이는 현상이 나타날 때도 있다. CSS3의 translate3d 속성을 사용하는 웹 페이지에 em 값의 절대치가 큰(-9999em,200em 등) 요소가 있으면 페이지 전체가 깜빡이게 된다. 이 때는 em값을 -70 ~70em의 값으로 바꾸거나 단위를 em에서 px로 수정하면 문제를 해결할 수 있다.

iOS 4에서 화면 깜빡거림

iOS 4는 1024 x 1024px의 텍스처(texture) 단위로 정보를 생성하는데, 1024 x 1024px 범위를 넘는 콘텐츠가 있을 때는 지속적으로 화면이 깜빡거리거나 애니메이션이 지연되는 문제가 생실 수 있다 iOS 5는 iOS 4보다 큰 텍스처 단위로 정보를 생성하기 때문에 문제가 발생되지 않는다.

iOS에서 발생되는 애니메이션 관련 문제의 더 많은 사례는 http://joehewitt.com/2011/10/05/fast-animation-with-ios-webkit/ 을 참고한다.
안드로이드 브라우저에서는 애니메이션 동작 후 링크를 눌렀을 때의 하이라이트가 정상적으로 나오지 않고, 이동 전 영역의 좌표가 터치한 영역으로 표시되는 문제가 있다.

이 문제를 해결하려면 다음과 같은 순서로 수정해야 한다.

애니메이션이 끝나면 자바스크립트로 지정한 CSS3 속성(translate, translate3d)을 해지하게 한다.
자바스크립트로 대상 요소의 위치(left,top,offset)를 다시 지정한다.
애니메이션 내부 영역 태그에서 focus() 메서드를 호출한다.

참고

<a> 태그에는 포커스가 이동해도 소프트 키보드가 나타나지 않기 때문에 내부 영역의 태그로는 <a> 태그를 사용한다.

1
<a href="javascript:void(0)" style="position:absolute;left:-9999px"></a>

그 밖에 translate3d 속성과 함께 다른 속성을 사용하면 애니메인션이 작동하지 않는 문제도 있다.

1
<div style="-webkit-transition:-webkit-transform 2s ease-out;-webkit-transform:translate3d(30px,0,0),rotate(45deg)">애니메이션</div>

자바스크립트 기반 애니메이션과 CSS3기반 애니메이션의 혼합

앞에서 설명한 자바스크립트 기반 애니메이션과 CSS3 기반 애니메이션은 각기 장단점이 있다. 이번에는 이 두가지 방법을 혼용해서 애니메이션을 구현하는 방법을 설명하겠다. 이 방법의 핵심은 애니메이션 주기는 자바스크립트로 구현하고 DOM 요소의 이동 및 변경은 CSS3로 구현하는 것이다. 이렇게 혼용한 방식을 적용하면 다음과 같은 장점이 있다.

CSS3를 사용해 DOM 요소를 이동하거나 DOM 요소의 상태를 변경하기 때문에 GPU 가속을 사용할 수 있고, 리렌더링이 발생하지 않는다.
안드로이드 브라우저에서 translate() 메서드나 translate3d() 메서드로 인해 발생하는 잔상 및 깜빡임 현상이 거의 발생하지 않는다.

구현하기

자바스크립트로 단위 시간마다 함수를 호출하고, 함수가 호출될 때마다 처리되는 DOM 요소의 이동 및 상태 변화는 CSS3를 사용하도록 애니메이션을 구성한다.

자바스크립트 방식 CSS3 방식을 혼합한 애니메이션은 대게 다음과 같은 순서로 구현한다.

1.애니메이션 대상이 되는 DOM 작성한다.

1
<div id="box" style="position:absolute;left:10px;border:1px solid red;width:100px;height:100px;" class="leftTranslate3d"></div>

2.원하는 FPS에 해당하는 단위 시간(ms)을 구한다.

3.애니메이션 대상에 트랜지션을 지정한다. 이 때 애니메이션 지속 시간(duration)은 0으로 지정한다.

1
2
3
4
5
6
<style>
.leftTransition{-webkit-transition:left 2s ease-out}
</style>
<body>
<div id="box" style="position:absolute;left:10px;border:1px solid red;width:100px;height:100px;" class="leftTranslate3d"></div>
</body>

4.애니메이션 대상을 이동하는 fAnimate() 함수를 작성한다. DOM 요소의 이동은 CSS3를 사용해 구현한다.

1
2
3
4
5
6
var fAnimate = function() {
nLength += 5;

elBox.style.webkitTransform = 'translate3d(' + nLength + 'px, 0px, 0)';
nAnimateTimer = setTimeout(1000/60);
};

5.setTimeout() 메서드 setInterval() 메서드, requestAnimationFrame() 메서드 등을 이용해 fAnimate() 함수를 단위 시간(ms)마다 호출한다. 다음은 setTimeout() 메서도로 아이디가 ‘box’인 DIV 요소를 단위 시간(약 16.7ms)마다 5px씩 오른쪽으로 이동하는 예다.

1
2
3
4
5
6
7
8
9
10
var elBox = document.getElementById('box'),
nAnimateTimer = null,
nLength = 0;,
fAnimate = function() {
nLength += 5;
elBox.style.webkitTransform = 'translate3d(' + nLength + 'px, 0px, 0)';
nAnimateTimer = setTimeout(1000/60);
};

nAnimateTimer = setTimeout(1000/60);

자바스크립트와 CSS3를 혼합한 애니메이션의 문제점

자바스크립트와 CSS3를 혼합해서 사용하기 때문에 이 방식 또한 자바스크립트 기반 방식에 있는 문제점을 똑같이 안고 있다.

애니메이션 시간 주기를 자바스크립트로 제어하기 때문에 구현이 어렵다.
시간 주기가 자바스크립트로 구현되기 때문에 CSS3 방식에 비해 애니메이션이 매끄럽지 못하다.
requestAnimationFrame API를 사용하지 못하는 지금의 모바일 환경에서는 setTimeout() 메서드나 setInterval() 메서드로 호출하기 때문에 시간의 정확성을 보장받지 못해 실제 애니메이션은 매끄럽지 못하거나 부자연스럽게 표현된다.
자바스크립트와 CSS3 혼합한 애니메이션은 자바스크립트에 비해 더 훌륭한 성능을 보장하고 CSS3 문제점을 해셜 할 수는 있는 방법이지만, 위에서 언급한 애니메이션 구현 방법 및 품질, 전력 소모에 대한 문제는 여전히 남아있다.

다음 표를 살펴보면 성능 면에서는 자바스크립트와 CSS3를 혼용하는 방법과 CSS3에 기반을 둔 방법이 가장 우수하고, 애니메이션 품질면에서는 CSS3 기반 애니메이션이 좋다. 구현 난이도 부분에서는 자바스크립트 기반 방식이 유리하다는 점을 알 수 있다. 하지만 절대적으로 모든 면에서 우수한 방법은 없다.

구분 자바스크립트 기반 CSS3 기반 자바스크립트 + CSS3 기반
동작 방식 브라우저는 애니메이션을
인지하지 못하고, 무조건
DOM과 스타일을 변경
하는 작업을 한다.
애니메이션 시작 시
브라우저가 애니메이션의
처음과 끝을 인지해서
작동한다.
애니메이션 처리 함수
호출 주기는 브라우저가
인지하지 못하나, 변경되는
대상의 상태 변화는 인지
할 수 있다.
성능 보통
단, requestAnimationFrame
API를 적용할 때 효과적이다.
좋음 좋음
GPU 가속 사용 못함 사용 사용
애니메이션 품질
구현 난이도
전력 소모 많음 GPU 가속시 현저히 증가 GPU 가속시 현저히 증가
안정성 좋음 안드로이드 기기에서
오류가 많음
iOS 기기에서는 최고의
성능과 품질을 제공함
좋음

향후 모바일 브라우저 개발 회사와 모바일 기기 제조사가 CSS3와 GPU 안정성을 확보함으로써 성능, 구현, 품질, 안정성이 뛰어난 CSS3 방식의 애니메이션을 사용할 수 있겠지만, 현재는 상황에 맞는 적절한 현실적인 대안을 찾아야 한다. 현재 가장 현실적인 대안은 운영체제와 모바일 기기에 따라 CSS3 방식 애니메이션을 사용하거나 자바스크립트와 CSS3를 혼용한 애니메이션을 사용하는 것이다.

물론 기기마다 약간의 특성이 있지만 다음과 같은 방법으로 애니메이션을 구현한다면 모바일 웹에서 최적의 애니메이션 성능을 나타낼 수 있을 것이다.

구현 방식 적용 운영체제와 모바일 기기
CSS3 CSS3 방식 애니메이션의 안정성이 확보된 iOS 기기
GPU 가속이 원활히 이워지는 삼정전자의 안드로이드 기기
자바스크립트와 CSS3 혼용 CSS3 방식 애니메이션의 안정성이 낮은 대부분의 안드로이드 기기
공유하기