웹 페이지로 애니메이션을 구현할 때 어떠한 방법으로 구현을 할 수 있을까?? 우리는 css의 animation, transition, transform 속성을 통해서 구현을 할 수도 있지만 사용자와의 복잡한 상호작용을 구현하기 위해서 javascript와 함께 애니메이션을 구현하기도 한다.
하지만 자바스크립트로 스타일 속성을 변화시키는 방법은 CSS보다 성능이 좋지 않다. 이번 포스팅은 애니메이션 관련 최적화 API인 requestAnimationFrame()
에 대한 사용법과 원리를 알아보는 시간을 알아보자.
프레임이란?
동영상을 물리적으로 환원하면 시간상 연속된 정지 사진들의 모음으로 볼 수 있는데, 이 각각의 정지 사진 1장을
프레임
이라 부른다. 이런 사진 토막들이 1초에 몇 장 보이느냐, 즉 프레임이 보이는 속도를 가리켜 'Frame Rate', 우리말로 옮기면 '프레임률'이라고 하며 국제 표준인 'Hz'로 쓰지만, 일상적인 경우 주사율과 구분하기 위해 fps를 쓴다
보통 인간의 눈은 1초에 60번 장면이 넘어가야 부드럽다고 느낀다고 한다. 그래서 현대 기기들은 시각적인 효과를 위해 초당 60번 화면을 다시 그리도록 기본적으로 설계된다고 한다.
굳이 FPS를 설명하고 Hz까지 설명하는 이유는 웹 화면에 부드러운 애니메이션 움직임 효과를 주기 위해선 이 프레임 단위에 맞게 설계해야 되기 때문이다. 초당 60개의 프레임을 렌더링한다는 말은, 16.666 밀리세컨드(1000ms / 60fps) 간격으로 프레임 생성이 필요한 셈이된다.
따라서 자바스크립트로 사용자에게 부드러운 애니메이션을 구현하려면 16.6ms밀리초 마다 코드를 호출하는 식으로 구현해야 한다.
16.6ms밀리초 마다 애니메이션 함수를 호출한다면 부드러운 애니메이션을 구현할 수 있지 않을까?
const performAnimation = () => {
/* 스타일 조정 스크립트 */
}
// 1초에 60번 무한 반복
setInterval(performAnimation, 1000 / 60)
그러나 setInterval()
와 setTimeout()
의 문제점은 주어진 시간 내에 동작을 할 뿐 프레임을 신경 쓰지 않고 동작한다는 점이다.
프레임에 맞게 동작하지 않는 이유
브라우저는 JavaScript 코드를 실행할 때이벤트 루프(Event Loop)
를 통해 작업을 처리합니다. 이 이벤트 루프에는 태스크 큐(Task Queue)가 있으며, setTimeout()이나 setInterval()과 같은 비동기 작업들이 일정 시간이 지난 후 이 큐에 들어갑니다. 하지만 이 작업들이 큐에 들어간 후 바로 실행되지 않을 수 있습니다. 그 이유는 브라우저가 먼저 현재 실행 중인 작업을 모두 마치고 나서야 큐에 있는 작업을 처리하기 때문입니다. 즉, 타이머 함수의 콜백이 큐에 들어가더라도, 그 시점에서 브라우저가 렌더링을 완료하지 않았거나, 이전에 실행 중이던 작업이 끝나지 않은 경우, 타이머 함수의 콜백이 프레임에 맞춰 실행되지 않을 수 있습니다.
window.requestAnimationFrame()
메서드는 브라우저에게 수행하기를 원하는 애니메이션을 알리고 다음 리페인트 바로 전에 브라우저가 애니메이션을 업데이트할 지정된 함수를 호출하도록 요청합니다. 이 메서드는 리페인트 이전에 호출할 인수로 콜백을 받습니다.
실제 화면이 갱신디어 표시되는 주기에 따라 함수를 호출해주기 때문에 자바스크립트가 프레임 시작 시 실행되도록 보장해주어 위와 같은 밀림 현상을 방지해준다.
codesandbox링크를 통해 setInterval과 requestAnimationFrame을 비교해보자
백그라운드 동작 중지
디스플레이 주사율에 맞게 호출
위의 화면과 같이 drag 시 마우스 좌표 상태를 변경하여 컨텐츠 좌표를 이동시키는 컴포넌트를 구현해본다고 가정해보자.
최적화 전 코드
const useMousePosition = ({
x = 0,
y = 0,
}: IUseMousePositionParameter) => {
const [mouseX, setMouseX] = useState(x);
const [mouseY, setMouseY] = useState(y);
const setMousePosition = ({ x, y }: IMousePositionParameter) => {
setMouseX(x);
setMouseY(y);
};
return {
mouseX,
mouseY,
setMousePosition,
};
};
최적화 한 후 코드 (requestAnimationFrame)
const useMousePosition = ({
x = 0,
y = 0,
}: IUseMousePositionParameter) => {
const [mouseX, setMouseX] = useState(x);
const [mouseY, setMouseY] = useState(y);
const setMousePosition = ({ x, y }: IMousePositionParameter) => {
requestAnimationFrame(() => {
setMouseX(x);
setMouseY(y);
});
};
return {
mouseX,
mouseY,
setMousePosition,
};
};
브라우저 Performance 창을 확인해본 결과 다음과 같았다.
Layout Shifts 란?
레이아웃 시프트(Layout Shifts)
란 늦게 로드되는 컨텐츠들로 인해, 먼저 로딩된 컨텐츠가 밀려나는 현상을 의미한다.
즉, rAF를 적용하기 전에는 마우스가 이동하는 모든 좌표를 그려야하기 때문에 Layout Shifts가 번번이 발생합니다. 하지만 rAF를 사용해 16ms마다 좌표를 최신화하기 때문에 Layout Shifts가 발생하지 않게 됩니다.
참고자료