[js] requestAnimationFrame 폴리필

개요top

어느 새, js보다는 css를 통한 애니메이션이 주류로 넘어갔습니다만, 여전히 미묘한 상태와 결합하는 애니메이션은 어쩔 수 없이 js에서 처리해야겠죠. js의 애니메이션 루프는 HTML5로 넘어오면서 과거의 setInterval이나 setTimeout으로부터 더욱 발전된 requestAnimationFrame으로 변경되었습니다.
이 메소드는 실제 화면이 갱신되어 표시되는 주기에 따라 함수를 호출해주기 때문에 굉장히 중요한 역할을 수행합니다. 특히 GPU가 연동되는 최신 브라우저들과 웹지엘 등 직접 GL컨텍스트를 사용하는 화면 갱신 시 더이상 GPU의 화면 변화와 동기화 되지 않는 Interval을 사용할 수도 없는 노릇입니다. 하지만 언제나 그렇듯 이 메소드도 구형 브라우저에서는 지원되지 않기 때문에 폴리필 과정을 통해야만 코드를 한 판으로 유지할 수 있습니다.

폴리필의 범위top

requestAnimationFrame은 인자로 함수를 하나 받고 반환값으로 id를 토하는데 이 id는 오직 0이 아니란 것만 보장할 뿐 어떤 값이다라고 특정할 수 없습니다.
http://www.w3.org/TR/animation-timing/#dfn-animation-frame-request-callback-list

중요한 건 인자로 받은 함수가 다음 렌더링 갱신 후에 호출되는데 이 함수가 그냥 호출되는게 아니라 인자로
DOMHighResTimeStamp라는걸 받게 되어있습니다.
http://www.w3.org/TR/hr-time/#domhighrestimestamp
위 문서를 참고해보면 DOMHighResTimeStamp를 일반적인 메소드로 얻으려면 performance.now() 를 사용할 수 있다는 것을 알 수 있습니다.

따라서 이상의 연관성을 추적해보면 다음과 같은 규모의 폴리필을 야기합니다.

  1. Date.now의 폴리필
  2. performance.now의 폴리필
  3. requestAnimationFrame의 폴리필

귀찮지만 이 3단계를 거쳐야 심리스하게 폴리필이 됩니다.
딱히 생략한다면 Date.now는 생략할 수 있습니다만, 그럼 반대로 performance.now구현이 분기되므로 그냥 폴리필하는 편이 낫습니다.

Date.now의 폴리필top

Date.now는 현재 시간을 timestamp값으로 반환하는 정적함수입니다. 내부 구현체가 직접 제공되면 좋은점은 현재시간을 얻기 위해 매번 새로운 Date객체를 생성할 필요가 없다는 점입니다. Date.now가 제공되지 않는 브라우저에서 가장 빨리 현재 시간을 얻는 코드는 다음과 같습니다.

currentTime = +new Date;
  1. 단항 연산자로서의 +는 new Date객체에 강제로 valueOf상황을 일으키는데
  2. Date의 valueOf는 getTime을 반환하므로 timestame값이 들어오게 됩니다.

위의 2단계 내부 함수 호출과정을 전부 js인터프리터 레벨로 내려보내기 때문에 직접 (new Date).getTime() 한 것보다는 약간 빠르게 됩니다(머 사실 이것도 브라우저마다 케바케입니다만 주 대상인 IE는 유리한 정도로..)
하지만 그래봐야 현재 시간을 얻을 때마다 짤없이 new Date 해야 합니다.

이러한 불편함과 낭비를 줄이고자 ES5스펙에서는 Date.now가 공식적으로 지원됩니다. 따라서 이에 대한 폴리필은 아래와 같습니다.

if(!Date.now){
  Date.now = function(){
    return +new Date;
  };
}
currentTime = Date.now();

performance.now의 폴리필top

원래 performance객체는 코드의 성능을 측정하기 위한 객체로 다양한 편의 속성과 메소드를 제공합니다만 이번 포스팅에서 다룰 필요도 폴리필 할 필요도 없으므로 오직 now메소드만 집중해보죠. performance.now메소드는 Date.now와 비교하여 두가지 차이점이 있습니다.

  1. 브라우저가 지원한다면 ms를 더욱 나눈 나노초(소수점이 표시되는 ms)를 제공한다. domhighrestimestamp는 현 시점에서 그냥 ms만 반환하는 브라우저도 있고 소수점까지 반환하는 브라우저도 있는 등 혼란상태입니다.
  2. Date.now는 현재 시간인데 비해, performance.now는 브라우저가 문서를 로딩한 시점으로부터의 경과시간을 표현한다.

폴리필 입장에서 1번의 차이는 큰게 아니지만 2번은 매우 중요합니다.

  1. 브라우저를 3시에 켜서
  2. 3시 10분에
  3. Date.now를 실행하면 3시 10분이 나오고
  4. performance.now를 실행하면 10분이라고 나온다는 뜻입니다.

이 차이가 매우 심각합니다. 따라서 performance.now가 없으면 스코프를 이용하여 이를 폴리필해야합니다.

//performance부터 만들어준다.
if(!window['performance']) window.performance = {};

if(!performance.now) performance.now = (function(){
  var offset = Date.now(); //브라우저의 실행시점을 기억한다.
  return function(){
    return Date.now() - offset; //경과시간을 반환한다.
  };
})();

스코프에 offset을 이용해 브라우저 시작시점을 기억해둔뒤 그 이후부터는 그 차이값으로 반환하여 최대한 performance.now와 유사하게 처리합니다.

requestAnimationFrame의 폴리필top

이제 마지막 단계에 돌입했습니다. 잔재료가 다 모였으니까 우선 기본빵으로 접두어막기를 해보죠.

if(!window['requestAnimationFrame']){
  window.requestAnimationFrame = window['webkitRequestAnimationFrame'] || 
    window['mozRequestAnimationFrame'] || 
    window['msRequestAnimationFrame'];

  //보통 raf가 없으면 cancel도 없으니까..
  window.cancelAnimationFrame = window['webkitCancelAnimationFrame'] ||
    window['mozCancelAnimationFrame'] ||
    window['msCancelAnimationFrame'];
}

이런 귀여운 막기는 폴리필이라고 하기엔 쑥스럽죠. 문제는 접두어를 다 돌려도 없는 경우입니다. 이 때 이용할 것은 setTimeout입니다. 헌데 그냥 setTimeout으로 함수를 지연시키면 그 함수가 호출은 되지만 첫 번째 인자로 domhighrestimestamp는 안들어옵니다. 따라서 전달된 함수를 한 번 더 감싸서 넘겨야합니다.
위의 접두어 부분을 생략하고 코드를 보죠.

window.requestAnimationFrame = function(func){

  //인자로 넘어온 함수를 감싸서 performance.now를 전달한다.
  var f = function(){
    func(performance.now());
  };

  //원래 requestAnimationFrame도 id를 넘기니까 setTimeout의 id를 반환하자.
  return setTimeout(f, 1000 / 60); //60fps정도로 처리하자
};

//의외로 cancel쪽은 이 정도다..
window.cancelAnimationFrame = clearTimeout;

이제 거의 폴리필이 완성되었습니다. 사실 기능적으로는 거의 동일하게 작동합니다. 작은 문제가 하나 있는데 보통 raf에 넘기는 함수는 한 번만 호출되는게 아니라 엄청나게 호출됩니다. 근데 위의 구조에서는 raf를 호출할 때마다 인자로 보낸 함수를 감싸는 새 함수를 매번 만들고 있습니다. 감당 안됩니다. 왠만하면 하기 싫지만 함수 객체에 본인을 감싼 함수를 기억하게 해두는 수밖엔 없습니다(이미 버린 몸^^)

window.requestAnimationFrame = function(func){

  //만약 인자로 넘어온 함수에 __raf__가 설정되어있지 않으면 만들자
  if(!func.__raf__){ 
    func.__raf__ = function(){
      func(performance.now());
    };
  }
  
  //func의 __raf__를 사용한다.
  return setTimeout(func.__raf__, 1000 / 60);
};

함수에 임의의 키를 잡아서 안타깝지만 구형브라우저에서 이것을 안하면 너무 성능이 낮아질 것이므로 어쩔 수 없네요 ^^;

결론top

requestAnimationFrame은 어느 새 js애니메이션 처리의 표준이 되어버렸습니다. 많은 라이브러리니 구글에 돌아다니는 코드가 허접하게 폴리필해서 도저히 쓸 수가 없다보니 직접 폴리필을 해버리고 말았네요.

더 나은 개선안을 갖고 계신 분들은 댓글에 노하우를 공유해주시면 감사하겠습니다.