Dynamic Control of Frame Rate with NgRx, RxJS

Jurung Park
Pagecall Engineering
5 min readDec 16, 2018

NgRx, RxJS를 이용하여 Canvas가 포함된 Web App에서 동적으로 렌더링 속도를 조절할 수 있도록 하였다.

배경

PageCall의 상태관리 전략

PageCall은 Redux 패턴으로 State를 관리하고 State의 변화를 Angular Component에서 Subscribe하여 View에 binding한다. 여기까지는 일반적인 NgRx를 사용하는 웹앱과 크게 다르지 않다. 하지만 PageCall에는 HTML Element로 표현할 수 없는 정보들이 있다. 이것들은 필기, 도형, 배경그림 처럼 HTML Canvas에 렌더링해야하는 요소들이다. 우리는 이런 정보들또한 State에 저장하고 관리한다. 하지만 이런 정보들은 Angular가 알아서 ChangeDetect 및 Binding 해주지 않는다. 그렇다면 우리에게 필요한 것은 무엇이었을까?

자체적인 ChangeDetect, Data Binding

Canvas에 렌더링되는 요소들을 사용자에게 보여줄 때는 Angular의 도움을 받지 못한다. 따라서 우리는 Graphic Component라는 자체적인 Angular Component를 만들었다.

Graphic Component without frame rate

코드 설명

위의 코드는 frame rate을 조절하지 않는 간단한 형태의 Graphic Component 이다. 외부로부터 lines$라는 Observable을 받아서 해당 Observable이 새로운 상태를 내뱉을 때마다 이전 상태와 차이점을 비교하여 Canvas에 렌더링한다.

  • lines$ : 바깥으로부터 받아오는 lines의 변화. 바깥쪽에서는 이 값을 NgRx Store에서 Select하여 Graphic Component에 제공해준다.
  • RenderService.drawDiff : 이전 상태와 다음 상태를 넘겨주면 변화를 감지하여 변화된 만큼의 그래픽요소들을 canvas에 그리거나, 변경하거나, 지운다.

drawDiff 는 비싼 함수

drawDiff는 꽤나 비싼 함수이다. 자체적인 ChangeDetection 알고리즘의 시간복잡도가 그렇게 높은 편은 아니지만 1초에 100번 이상 실행되면 부담이 될 수 있다. 1초에 100번이상 그려줘도 사용자들은 느끼지도 못하는데 말이다!

일반적인 Redux change detection 알고리즘과 유사하다.

그런데.. 1초에 100번이상 상태가 변하는게 말이 되나?

일반적인 Web app은 말이 안될 수 있다. 대부분의 상태변화는 사용자의 조작으로 인해 발생하며 사용자가 1초에 100번의 조작을 가할 수 있는 경우는 거의 없다. 사용자가 초당 조작할 수 있는 최대 횟수는 입력장치(마우스나 키보드)의 주사율 일텐데 보통 초당 60회 정도의 주사율을 가지고 있기 때문에 초당 100번 이상 상태가 변하는 상황을 걱정하는 건 과할 수 있다는 말이다.
하지만 PageCall에서는 여러명의 사용자들이 상태를 변경하고 그것이 P2P로 직접 전달이 되기 때문에 쉽게 일어날 수 있는 일이다. 예를 들어 3명의 사용자가 60Hz의 펜마우스를 가지고 각자 글씨를 쓰고 있다면 1초에 180번의 상태변화가 일어난다.

해결 전략

쉬운 해결방법

사실 RxJS의 sampleTime이라는 operator를 사용하면 손쉽게 해결할 수 있다.

Graphic Componet with frame rate (easy)

수정된 건 pairwise operator전에 sampleTime이 추가된 것 뿐이다. 이 한줄을 추가하여 drawDiff가 1초에 많아봐야 60번까지만 실행되도록 했다. 예전에는 lines가 바뀌는 족족 변화를 계산하고 렌더링해줬다면 이제는 17ms 정도의 변경사항을 모아놨다가 한꺼번에 처리한다. 그런데 frame rate가 60으로 고정되어 있다는 문제가 있다. 성능이 낮은 기기에서는 60Hz조차도 부담이 될 수 있기 때문이다. 사용자의 기기 성능에 따라 frame rate이 동적으로 변하도록 할 수 있을까?

Advanced 해결방법

frame rate을 Input으로 받자!

자 이제 sampleTime 대신 sample operator를 사용했다. 둘의 차이점은 무엇일까?

  • sampleTime : 정해진 시간 간격으로 sampling.
  • sample : 넘겨준 Observable의 신호에 따라 sampling.

frameRate가 동적으로 변할 수 있기 때문에 정해진 시간 간격에 따라 샘플링을 수행하는 sampleTime은 사용하기에 부적절하다. 따라서 frameRate가 변경됨에 따라 적절한 시간 간격으로 샘플링 신호를 발생시키는 Observable이 필요했다. 그것이 바로 코드에서 보이는 frameControl$ 이다. frameRate의 변화에 따라 적절한 간격의 신호를 발생시키는 것은 switchMap의 특성을 이용하였다.

frameRate값은 NgRx Store에서 저장/관리 되기 때문에 여러가지 앱 상황에 따라 앱 여러 곳에서 간단히 조절할 수 있다.

마무리

RxJS는 정말 좋다. RxJS가 없었다면 이런 복잡하고 비동기적인 작업들을 어떻게 자연스럽게 처리했을까? Observable 패턴을 이해하고 익히는 것은 현재 프론트엔드 웹 개발자들에게 꼭 필요한 일인 듯 하다.

--

--