팀 레이어
레이어 블로그
Sign In
Home
막상 생각했던 회고, 불편했던 적은 없으신가요?
레이어 블로그
팀 레이어 기술 블로그
Subscribe
팀 레이어 기술 블로그

자바스크립트로 스타일을 변경한다면 rAF(requestAnimationFrame)를 사용해보세요!

현우
Oct 23, 20258m ago
카테고리
  1. 프론트엔드

글을 시작하며

보통 웹 페이지의 애니메이션을 구현할 때, 리페인트를 방지하고자 CSS의 animation, transition, transform 속성을 통해 구현을 할 수 있지만 보다 사용자와의 복잡한 상호작용을 구현하게 하기 위해서 자바스크립트를 활용해 스타일을 변화시켜야하는 경우도 있어요. 특정 영역을 클릭하거나, 웹 페이지를 스크롤 할 때 변화무쌍한 애니메이션 작업들이 그러해요.
간단하고 규칙적인 애니메이션은 CSS로만 요소의 좌표값이나, 스타일 크기를 변화시키고 세밀하고 디테일한 변화를 필요로하는 애니메이션의 경우에는 자바스크립트로 스타일 속성을 변경시키는 편인데요, 하지만 자바스크립트로 스타일을 변경시키게 되면 대개 너비 또는 위치 값을 변경시키는 요소들이기에 일반적인 CSS보다 성능이 좋지 못해요. 따라서 어쩔 수 없이 자바스크립트와의 상호 협력이 필요한 경우에는 이를 위한 최적화 기법이 필요해요.

브라우저 렌더링 단계

위에서 언급했듯 리페인트가 무엇인지, 리플로우가 무엇인지 한번 확인을 해볼게요.
브라우저는 위와 같은 과정들을 통해 브라우저 파이프라인을 그려요.
•
JavaScript : 애니메이션 및 기타 작업 스크립트를 수행 (DOM 생성)
•
Style : CSS 규칙을 어떤 요소에 적용할지 계산하는 과정 수행 (CSSOM 생성)
•
Layout (Reflow) : 브라우저는 DOM과 CSSOM을 결합해 객체들의 위치와 크기 등을 계산하는 렌더 트리를 생성
•
Paint (Repaint) : 브라우저는 렌더 트리를 사용해 실제로 화면에 픽셀을 출력 (객체가 실제 화면에 그려지는 것을 의미)
•
Composite : 브라우저는 화면에 출력되는 객체들을 합성해 최종 화면을 생성 (최적화 과정)

브라우저 프레임

60hz, 120hz과 같은 모니터 주사율이 있다고 했을 때, 이는 1초 동안 모니터 화면의 출력 빈도를 의미해요. 영화 또는 애니메이션을 보는 과정은 사실 짧은 간격으로 이루어진 사진을 이어보는 과정이에요. 이 각각의 장면을 frame이라고 칭하는데, 특정 시간 내에 보여지는 frame 갯수를 frame rate 혹은 frame per second (fps)라고 해요. 보통 사람의 눈은 1초에 60번 장면이 넘어가야 부드럽게 느낀다고 하며, 그래서 현재 기기들은 시각적인 효과를 위해 초당 60번 화면을 다시 그리도록 기본적으로 설계되어있어요. 이를 60fps 혹은 60hz라고 불리우는 이유예요.
웹 화면에서 부드러운 효과를 제공하기 위해서는 이 프레임 단위에 맞게 설계해야하기 때문에, 초당 60개의 프레임을 렌더링 한다는 말은 16.666ms(1000ms / 60fps) 간격으로 프레임 생성이 필요한 셈이 돼요. 따라서 자바스크립트로 사용자에게 부드러운 애니메이션을 구현하려면 16.6ms 마다 코드를 호출하는 식으로 구현해야해요.

타이머 함수로 60fps 제공하기

아래와 같이 setInterval과 setTimeout을 통해 60fps를 제공하는 코드를 작성해보았어요.
const executeAnimation = () => {
    // ... script
}
setInterval(executeAnimation, 1000 / 60);

const executeAnimation = () => {
    setTimeout(executeAnimation, 1000 / 60);
}
setTimeout(executeAnimation, 1000 / 60);
위와 같이 타이머 함수를 작성하게 되면 주어진 시간 내에 동작은 하겠지만, 프레임을 신경쓰지 않고 동작하는 문제점이 발생한다. 타이머 함수는 프레임 단위로 프레임 시작 시간에 맞춰 실행됨을 보장하기 못하기 때문이에요.
약 16ms 간격으로 프레임 단위가 진행되어야하는데, 만약 브라우저가 다른 작업 수행으로 인해 지연되어 자바스크립트의 콜백 코드가 프레임 단위 중간에서 호출되면 자바스크립트 실행에 의해 리플로우가 발생하면서 브라우저 렌더링 단계인 레이아웃 - 페인트 - 합성 과정이 다시 일어나는데, 이때 프레임이 생성되지 못하고 누락되어 프레임이 깎이는 현상이 발생할 수 있기 때문이에요.
위와 같은 이유 때문에 프레임 드랍이 일어나 지연 현상이 발생하고, 화면이 버벅이는 현상이 발생한다. 이를 해결하기 위해 대안으로 나온 것이 rAF (requestAnimationFrame)이에요.

requestAnimationFrame

rAF(requestAnimationFrame) 함수는 시스템이 프레임을 그릴 준비가 되면 애니메이션 프레임을 호출하여 애니메이션 웹 페이지를 보다 원활하고 효율적으로 생성할 수 있도록 해줘요. 실제 화면이 갱신되어 표시되는 주기에 따라 함수를 호출해주기 때문에 프레임 시작 시 콜백이 실행되도록 보장해주어 프레임 드랍과 같은 밀림 현상을 방지해줘요. rAF(requestAnimationFrame) 함수가 실행되면 브라우저는 다음 프레임이 그려지기 전에 함수를 실행하도록 예약하기 때문에 각 프레임이 정확히 16.6ms 간격으로 렌더링되게 돼요.
애니메이션 프레임을 페인팅할 준비가 되었을 때 호출한다는 것은, 이전 비교했던 setInterval 방식이 준비가 되지 않아도 페인팅을 요청하는 것과 대비되면서, 보다 웹 브라우저에 최적화되어 있다고 말할 수 있어요. 또한 지연 및 블로킹 현상이 생기지 않아 부드러운 흐름을 제공할 수 있어요.

rAF(requestAnimationFrame)은 어떤 특징을 가지고 있어요?

•
백그라운드 동작 중지
setInterval 같은 경우에 브라우저의 다른 탭 화면을 보거나 브라우저가 최소화되어 있을 때 계속 타이머가 돌아 콜백을 호출하기 때문에 시스템 리소스 낭비를 초래하고, 불필요한 전력을 소모하게 만들어요. 반면 requestAnimationFrame은 페이지가 비활성화 된 상태라면 페이지 화면 그리기 작업도 브라우저에 의해 일시 중지됨으로 CPU 리소스를 줄일 수 있어요.
•
디스플레이 주사율 최적화 호출
최적화된 웹 애니메이션을 구현하는데 있어 setInterval과 requestAnimationFrame의 결정적인 차이점인 특징 중 하나이다. 현재 기기들이 기본적으로 60hz로 설계가 되어있어 1초에 60번 호출한다고 했지만, 이 또한 자신의 모니터 주사율에 따라 달라요. 웹 브라우저는 디스플레이의 주사율을 따르며 만일 144hz 주사율 고사양 모니터일 경우에 rAF 역시 1초에 144번 호출되게 돼요.
setInterval에서는 콜백 함수 호출 간격을 시간을 지정해주고 호출 횟수를 설정하지만, rAF에서는 모니터의 주사율을 그대로 따르게 되어 최적화되어 있어요. 그리고 이러한 특성 때문에 rAF는 스크롤 이벤트 최적화 기법으로도 쓰이기도 해요.

rAF(requestAnimationFrame)은 처리하는 큐도 다르다?

rAF(requestAnimationFrame)은 setTimeout이나 이벤트 핸들러와 같이 "애니메이션 프레임"을 그리기 위한 콜백 함수를 등록하고 비동기 task로 분류하여 처리돼요. 이때 rAF(requestAnimationFrame)은 일반적인 태스크 큐에서 처리되는 것이 아닌, animation frame 이라는 별개의 queue에서 처리된다는 점에서 차이가 있어요.
Animation Frames는 브라우저의 렌더링 엔진이 다음 프레임을 그리기 전에 실행해야하는 rAF(requestAnimationFrame)에 등록한 콜백 함수를 담는 별도의 큐예요. 별도의 큐에서 적재되어 이벤트 루프에 의해 꺼내지기 때문에 실행이 뒤쳐지거나하는 현상을 감소시킬 수 있어요. 단, rAF(requestAnimationFrame)도 브라우저의 CPU나 GPU 사용량 여부 등에 따라 콜백 함수 실행이 밀릴 수 있어요.

rAF(requestAnimationFrame)의 사용 방법

rAF(reqestAnimationFrame)은 setTimeout처럼 콜백 함수 내부에서 재귀 호출하는 식으로 구성하면 돼요. 브라우저는 애니메이션 프레임을 출력할 때마다 rAF(requestAnimationFrame)에 등록된 콜백 함수들을 비동기로 호출하여, 애니메이션을 부드럽게 출력해줘요. 여기서 차이점은 setTimeout은 타이머를 지정해줘야하지만, rAF(reqestAnimationFrame)은 프레임 단위로 동작하기 때문에 별도의 반복을 위한 플래그가 필요없어요.
const executeAnimation = () => {
	...any script
	
	requestAnimationFrame(executeAnimation);
}

requestAnimationFrame(executeAnimation);
그리고 setTimeout을 취소하기 위해 clearTimeout을 사용하듯이, rAF(requestAnimationFrame)을 취소하기 위한 방법으로 cAF(cancelAnimationFrame)을 사용해요.
응용 예제로 rAF(requestAnimationFrame) 함수의 콜백 함수의 인자로 매개변수를 전달하게 되면, rAF(requestAnimationFrame) 함수가 실행되기 시작한 이후의 시간을 얻을 수 있어요. 이를 통해 애니메이션 수행 시간을 구해 특정 타이머일 때 스크립트를 실행하는 식의 응용이 가능해요.
let startTime;

const draw = (timestamp) => {
	if (!start) start = timestamp;
	currentTime = timestamp - startTime;
	
	if (currentTime > 2000) {
		console.log('The End');
		return;
	}

	requestAnimationFrame(draw);
};

requestAnimationFrame(draw);
팀
Subscribe to '팀 레이어'
Subscribe to my site to be the first to receive notifications and emails about the latest updates, including new posts.
Join Slashpage and subscribe to '팀 레이어'!
Subscribe
👍