2016년 10월 23일 일요일

RxJS 에서 requestAnimationFrame 을 사용하려면?

http://jsbin.com/qosigic/edit?html,js,output

소스는 이쪽.

게임과 같은 분야는 타이밍 처리가 매우 중요하기 때문에 UI를 갱신하는 무한 루프를 놓고 delta 시간을 얻어와서 처리하는 로직을 많이 쓰는데
이걸 Javascript 에서 할 땐 전통적으로 setTimeout 을 걸어놓은 함수를 재귀하는 식으로 썼는데 문제는 setTimeout(setInterval도 마찬가지)이 굉장히 부정확한 타이밍을 가지고 있었고 이를 보완하기 위해 requestAnimationFrame 이라는 것을 만들어 사용하기 시작했다.
문제는 역시 지원하지 않는 몇몇 브라우저(ex. IE < 10)들 때문에 안타깝게도 RxJS 5.x 의 defaultScheduler에선 setTimeout(https://github.com/Reactive-Extensions/RxJS/blob/6dac0365ad22f87a92197fc4dcee70e72a11ddbb/src/modular/scheduler/defaultscheduler.js#L119)을 쓰고 있는데
물론 권장하는 방식은 https://github.com/Reactive-Extensions/RxJS/tree/8fa95ac884181fb6cbff8ce7c1d669ffb190f5e4/examples/crop 의 예처럼 RequestAnimationFrameScheduler 를 사용하는 것이지만

RxLua 처럼 기본으로 타이머가 없고 외부패키지에 의존하는 환경도 있고해서
기본 RxJS 환경에서 직접 만들어보기로 했다.

Scope 을 위해 일단 function으로 감싸보았다.
(function() {
  var canvas=document.getElementById('canvas1');
  var ctx = canvas.getContext('2d');
  var start = 0;
  var step = function(timestamp) {
    if (!start) start = timestamp;
    var progress = timestamp - start;
    ctx.clearRect(0,0,canvas.width,canvas.height);
    ctx.fillText("legacy: "+progress.toFixed(2), 10,10);
    window.requestAnimationFrame(step);
  }
  window.requestAnimationFrame(step);
})();
이게 아마 보통 requestAnimationFrame 을 사용하는 패턴일 것이다.
그러면 Rx 에서 subscribe 일때 처리해야하는 부분은 어디일까?
실제로 UI를 갱신하는 ctx 부분일 것이다.
(function() {
 var canvas=document.getElementById('canvas1');
 var ctx = canvas.getContext('2d');
 var start = 0;
 var step = function(timestamp) {
   if (!start) start = timestamp;
   var progress = timestamp - start;
   /* 여기부터 */
   ctx.clearRect(0,0,canvas.width,canvas.height);
   ctx.fillText("legacy: "+progress.toFixed(2), 10,10);
   /* 여기까지 */
   window.requestAnimationFrame(step);
 }
 window.requestAnimationFrame(step);
})();
이 부분을 분리해보자.
가장 간단한 방법은 역시 Subject 로 만드는 방법이겠다.
stepSubject.subscribe(progress=>{
  ctx.clearRect(0,0,canvas.width,canvas.height);
  ctx.fillText("Rx Subjeect: "+progress.toFixed(2), 10,10);
});
이렇게 실제로 progess 가 넘어오는 것을 받는 subscribe 를 만들고
stepSubject 를 생성한 뒤 루프 안에서 stepSubject.next 에서 progress 를 건내주도록 하자.
(function() {
  var canvas=document.getElementById('canvas2');
  var ctx = canvas.getContext('2d');
  var stepSubject = new Rx.Subject();
  var start = 0;
  var step = function(timestamp) {
    if (!start) start = timestamp;
    var progress = timestamp - start;
    stepSubject.next(progress);
    window.requestAnimationFrame(step);
  }
  window.requestAnimationFrame(step);
  stepSubject.subscribe(progress=>{
    ctx.clearRect(0,0,canvas.width,canvas.height);
    ctx.fillText("Rx Subjeect: "+progress.toFixed(2), 10,10);
  });
})();
간단하다. 별로 크게 건들지 않고 분리를 했다.

scope을 생각했을 때 반복하는 step function이 Observable 안에 있으면 좋겠다 싶은 생각이 든다.
start 와 step을 Observable.create 안에 넣고 .next 로 쏘아보자. subscribe는 동일하다.
(function() {
  var canvas=document.getElementById('canvas3');
  var ctx = canvas.getContext('2d');
  var stepObservable = Rx.Observable.create(observer=>{
    var start = 0;
    var step = function(timestamp) {
      if (!start) start = timestamp;
      var progress = timestamp - start;
      observer.next(progress);
      window.requestAnimationFrame(step);
    }
    window.requestAnimationFrame(step);
  });
  stepObservable.subscribe(progress=>{
    ctx.clearRect(0,0,canvas.width,canvas.height);
    ctx.fillText("Rx Observable: "+progress.toFixed(2), 10,10);
  });
})();
이렇게 옮기면 Rx.Observable 안에서 requestAnimationFrame 을 제어할 수 있다.
개인적으로는 이런 상황이라면 start 와 step 의 Observable 안에 scope 을 가지고 있는 후자를 더 추천한다.