이마고웍스의 Dentbird Crown은 AI 기술을 활용해 몇 번의 클릭만으로 단시간에 크라운 디자인의 전 과정을 마칠 수 있는 인공지능 기반 웹 덴탈 CAD 솔루션입니다.
이마고웍스에서 릴리즈 계획중인 Crown004 프로젝트에 참여하며, 3D 모델의 단면을 렌더링하는 2D Cross Section 신규 기능의 이벤트 처리 스케줄링을 최적화하여 FPS를 향상시켜보았습니다.
const handleRotationChange = (e: Event, value: number | number[]) => {
// do something
};
위의 코드는 2D Cross Section Box의 Slider를 조절하여 카메라의 회전을 처리하는 기능입니다. 이 방식은 기능적으로는 문제가 없으나, 실제 사용 시에는 약간의 문제가 발견되었습니다.
위의 GIF에서 확인할 수 있듯이, Slider를 조절할 때 FPS(Frames Per Second)가 급격하게 떨어지는 현상이 발생했습니다. 원래의 60fps에서 20~40fps까지 하락하는 프레임 드랍 문제가 있었습니다.
- 디바운싱(debouncing): 연이어 호출되는 함수들 중 마지막 함수(또는 제일 처음)만 호출하도록 하는 것
- 쓰로틀링(throttling): 일정 시간 동안 이벤트 핸들러를 한 번만 실행하도록 제어하는 것
프레임 드랍 문제를 해결하기 위해 처음 시도했던 방법은 setTimeout을 이용한 디바운싱 기법이었습니다. setTimeout(callback, 0) 메서드는 주어진 콜백 함수를 이벤트 루프의 태스크 큐에 추가합니다. 이렇게 설정하면 현재 실행 중인 모든 스크립트 또는 작업이 완료된 후에 해당 콜백이 실행됩니다.
let debounceTimeout: NodeJS.Timeout;
const handleRotationChange = (e: Event, value: number | number[]) => {
clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(() => {
// do something
}, 0);
};
디바운싱은 연속적인 이벤트 호출을 최소화하고 마지막으로 발생한 이벤트만 처리하도록 합니다. 이를 구현하기 위해, 이벤트가 발생할 때마다 setTimeout을 설정하여 지연 후 코드를 실행합니다. 만약 지정된 지연 시간 내에 동일한 이벤트가 다시 발생하면, 그 이전에 설정된 setTimeout을 clearTimeout을 사용해 취소하고 새로운 setTimeout을 설정합니다.
clearTimeout(debounceTimeout);
는 이전에 설정된 setTimeout을 취소하는 역할을 합니다. 이렇게 함으로써 연속적으로 발생하는 이벤트에 대해서는 중간의 이벤트들을 무시하고 마지막 이벤트만을 처리하게 됩니다
이 방법을 통해 FPS를 향상 55-60fps로 향상 시켰습니다. 그러나 팀원의 리뷰를 통해 이 방식에는 몇 가지 문제점이 존재한다는 것을 알게 되었습니다.
setTimeout(callback, 0)
는 잘 조정하면 최대 FPS 근처에서 동작할 수 있습니다.
많은 작업이 큐에 존재하거나 여러 번 setTimeout(callback, 0)
이 연속적으로 호출되면, 콜백이 실행되기까지 지연이 생기게 되어 UI의 반응성이 떨어질 수 있습니다.
왜냐하면 싱글 스레드로 동작하는 Javascript 엔진은 setTimout의 비동기 API를 처리하기 위해 task queue에 쌓아서 하나씩 꺼내서 처리합니다. 만약 큐에 많은 작업이 쌓일 경우 setTimout API가 제대로 trigger 되지 않을 수 있습니다.
이러한 피드백을 바탕으로, 다음 단계에서는 requestAnimationFrame을 사용한 디바운싱 기법을 적용하여 성능 최적화를 시도하게 되었습니다.
requestAnimationFrame은 브라우저의 다음 리프레시 주기에서 수행할 작업을 스케줄링하는데 사용됩니다. 대부분의 디스플레이는 60Hz로 작동하므로, requestAnimationFrame 콜백은 대략 16.7ms 간격으로 실행됩니다.
requestAnimationFrame은 내장된 디바운싱 능력을 가지고 있습니다. 여러 번 requestAnimationFrame이 연속으로 호출되더라도, 브라우저는 다음 화면 갱신 주기까지 기다렸다가 마지막으로 등록된 콜백만 실행합니다.
이로인해 setTimeout(callback, 0)에 비해 안정적이며, 애니메이션, 렌더링 및 그 외 화면 갱신과 관련된 작업에 효과적으로 화면 갱신을 관리할 수 있습니다.
let rafId: number | null = null;
const handleRotationChange = (e: Event, value: number | number[]) => {
if (rafId !== null) {
cancelAnimationFrame(rafId);
}
rafId = requestAnimationFrame(() => {
// do something
});
};
cancelAnimationFrame(rafId);
는 이전에 요청된 requestAnimationFrame을 취소합니다. rafId는 requestAnimationFrame의 반환값으로, 특정 requestAnimationFrame 요청을 식별하는 ID입니다.
requestAnimationFrame과 setTimeout은 각각의 장단점이 있고, 어떤 방법이 더 적합한지는 상황에 따라 달라집니다. 현재 프로젝트에서는 작업의 특성을 종합적으로 고려하여 requestAnimationFrame의 방법을 채택하기로 결정했습니다.
requestAnimationFrame을 사용함으로써 FPS가 45-55 사이로 향상시켰습니다. 애니메이션과 화면 렌더링 작업이 더 안정적으로 이루어짐과 동시에 들어오는 다른 인터렉션을 받아들일 수 있는 정도의 성과를 얻었습니다.
잘읽었습니다.