최근에 이벤트 루프의 개념을 다시 짚어보던 중에 requestAnimationFrame을 접하게 되어 좀 더 자세히 알아보았습니다.
효율적인 페이지 렌더링이나 매끄러운 애니메이션과 인터랙션 구현에 관심 있으시다면 requestAnimationFrame을 활용해보셔도 좋을 것 같아요. requestAnimationFrame은 웹 애니메이션을 구현할 때 자주 사용되는 Web API 중 하나입니다. 우선 어떤 방식으로 사용하는지 살펴보겠습니다.
window.requestAnimationFrame() 메서드
프레임(frame)은 화면에 표시되는 이미지 단위를 의미합니다. 움직이는 애니메이션을 보여주기 위해서는 연속적으로 조금씩 변화하는 이미지를 표시해서 하나의 연속된 동작으로 인식하도록 합니다.
requestAnimationFrame 메서드는 브라우저가 애니메이션 프레임을 최적의 시간에 렌더링하도록 스케줄링하는 API입니다. 브라우저가 다음 repaint를 수행하기 전에 실행할 애니메이션 콜백 함수를 호출하도록 요청합니다. 보통 1초에 60회 호출하는데 대부분의 브라우저에서는 화면 디스플레이 주사율에 따라 다릅니다(주사율이 120hz이라면 1초에 120번 호출).
브라우저의 갱신 속도에 맞춰 애니메이션 루프가 실행되기 때문에 불필요한 프레임이 생성되지 않습니다.
그리고 브라우저에서 비활성화된 탭이나 보이지 않는 요소에 대한 애니메이션은 실행되지 않기 때문에 자원 사용을 최소화하고 배터리 수명을 연장하는 데 도움이 됩니다.
기본적인 사용 방법은 다음과 같습니다. 사용법 자체는 복잡하지 않습니다.
function animate() {
// 애니메이션 로직 작성(e.g. DOM 요소의 위치 업데이트)
requestAnimationFrame(animate); // 다음 프레임을 위해 자기 자신을 호출
}
requestAnimationFrame(animate); // 애니메이션 시작
다음은 requestAnimationFrame을 활용하는 간단한 애니메이션입니다.
프레임별로 요소의 이동 거리를 설정하여 애니메이션 속도를 조절할 수 있습니다. 애니메이션 속도 조절과 관련된 구체적인 예시는 이곳을 참고해 주세요.
See the Pen requestAnimationFrame example by Yuri Kim (@yurikim) on CodePen.
애니메이션을 중지하거나 더 이상 필요하지 않을 때에는 requestAnimationFrame을 호출했을 때 반환하는 식별자를 cancelAnimationFrame 함수에 전달하여 애니메이션 프레임 요청을 취소할 수 있습니다.
// 애니메이션 프레임 요청 식별자를 저장할 변수
let animationFrameId;
function animate() {
// 애니메이션 로직
// 애니메이션을 중지해야 할 때 stopAnimation() 함수 호출
requestAnimationFrame(animate);
}
animationFrameId = requestAnimationFrame(animate); // 애니메이션 시작
// 애니메이션 중지
function stopAnimation() {
cancelAnimationFrame(animationFrameId);
}
타이머 함수 대신 requestAnimationFrame을 사용해야 하는 이유
setTimeout이나 setInterval과 같은 타이머 함수를 이용해서 일정 시간마다 코드를 반복 호출해도 애니메이션을 구현할 수 있습니다.
보통 인간의 눈은 1초에 60번 프레임이 넘어가야 부드럽게 느끼므로, 웹 화면에서 부드러운 애니메이션을 구현하려면 프레임 단위에 맞게 초당 60개의 프레임을 렌더링해야 합니다. 즉, 자바스크립트로 약 16.6ms(1000ms / 60fps) 간격으로 코드를 호출하는 식으로 구현해야 하는데요.
타이머 함수는 프레임과 무관하게 임의로 지정한 콜백 함수 호출 간격에 따라 동작하기 때문에 requestAnimationFrame과 달리 프레임 단위로 실행되는 것을 보장하지 못합니다. 특히 여러 작업을 동시에 처리하고 있는 경우에 다른 작업을 수행하느라 지연되어 애니메이션과 관련된 콜백이 프레임 시작 시간에 맞춰 실행되지 않으면 화면이 버벅거리게 됩니다. 그리고 브라우저의 다른 탭에 접속하거나 최소화하더라도 타이머가 계속 작동하여 콜백을 호출하므로 시스템 리소스가 낭비될 수 있습니다.
반면 requestAnimationFrame은 화면이 갱신되어 표시되는 주기에 따라 콜백 호출을 스케줄링하기 때문에 프레임 시작 시 자바스크립트가 실행되도록 보장해주어 버벅거림을 방지해줍니다. 특히 앞에서 언급한 것처럼 화면 디스플레이 주사율에 따라 호출 간격이 정해지기 때문에 애니메이션 구현에 최적화되어 있다고 할 수 있습니다.
따라서 단순 반복 작업 또는 지연이 필요하거나 정확한 시간 간격에 따라 코드를 실행해야 하는 경우에는 타이머 함수를, 부드러운 애니메이션과 백그라운드 및 비활성화 탭 최적화를 원하는 경우에는 requestAnimationFrame을 사용하면 되겠습니다.
이벤트 루프는 requestAnimationFrame 콜백을 어떻게 처리할까
이벤트 루프는 싱글 스레드로 동작하는 자바스크립트 엔진에서 여러 비동기 작업들을 효율적으로 관리합니다. 여기에서 requestAnimationFrame은 일반적인 태스크 큐가 아니라 별도의 큐인 Animation frames에서 처리됩니다.
Task Queue(Macrotask Queue)에는 타이머 함수(setTimeout, setInterval 등), I/O 작업, 유저 인터랙션 등의 태스크가 담겨 있습니다.
Microtask Queue에는 Promise에 의해 처리되는 작업이나 MutationObserver 콜백이 큐에 담겨 실행을 기다립니다.
Animation Frames에는 requestAnimationFrame 콜백이 담겨 있으며 브라우저가 화면을 다시 그릴 준비가 되었을 때 작업이 실행됩니다.
이벤트 루프는 다음 순서로 한 사이클이 동작합니다.
- 현재 실행 중인 Task(Macrotask)가 있으면 그 태스크를 완료합니다.
- 이벤트 루프가 Microtask Queue를 확인하고 Microtask Queue에 있는 모든 작업을 처리할 때까지 루프를 돕니다.
- 마이크로태스크가 모두 처리되면 requestAnimationFrame에 의해 등록된 콜백이 있을 경우 실행합니다.
- 렌더링이 발생하며 이때 페이지의 reflow나 repaint가 이뤄집니다.
- Macrotask Queue에서 다음 태스크를 선택하여 실행하고 다시 1번부터 사이클을 반복합니다.
3번과 4번 과정에서 확인할 수 있듯이 requestAnimationFrame은 브라우저의 paint 주기와 동기화되기 때문에 브라우저가 화면을 갱신할 준비가 되었을 때 콜백을 실행하게 됩니다. 더 부드럽고 자연스럽게 애니메이션을 구현할 수 있는 이유와 직결됩니다.
requestAnimationFrame를 잘 활용하여 데이터 대시보드나 차트에서 대량의 데이터 업데이트를 부드럽게 처리하거나 드래그 앤 드롭, 페이지 스크롤 등 인터랙티브 UI나 웹 기반 게임에서도 응용해볼 수 있을 것 같습니다.
이벤트 루프의 구조에 대해 전반적으로 짚어볼 수 있었습니다. 프레임과 관련된 설명이 많았는데 이와 관련된 브라우저 갱신과 VSync에 대해서도 좀 더 알아보고 requestAnimationFrame과 연결지어 이해해보려고 합니다.
참고 자료
- MDN Window.requestAnimationFrame() method
- MDN WIndow.cancelAnimationFrame() method
- DEVIEW 2015 브라우저는 VSync를 어떻게 활용하고 있을까
- 웹 애니메이션 최적화 requestAnimationFrame 가이드
- 모던 자바스크립트 튜토리얼 JavaScript animations
'Web' 카테고리의 다른 글
tailwind-merge로 클래스 충돌 해결하기 (0) | 2024.04.11 |
---|---|
IndexedDB 활용해보기 (0) | 2024.03.31 |
가상 DOM 없이 효율적으로 DOM 요소 추가하기 (feat. DocumentFragment 노드) (0) | 2022.10.31 |