requestAnimationFrame으로 DOM 읽기와 쓰기 분리하기
프론트엔드 성능을 이야기할 때 requestAnimationFrame은 자주 등장합니다. 하지만 단순히 “애니메이션에는 rAF를 쓰면 된다” 정도로만 이해하면 실제 성능 개선으로 이어지기 어렵습니다.
중요한 건 requestAnimationFrame 자체보다 브라우저가 언제 레이아웃을 계산하고, 언제 화면에 반영하는지를 예측 가능하게 만드는 것입니다. 특히 DOM을 수정한 직후 레이아웃 값을 읽는 패턴은 성능 문제의 흔한 원인이 됩니다.
requestAnimationFrame은 언제 실행될까?
requestAnimationFrame은 브라우저가 다음 화면을 그리기 직전에 콜백을 실행하도록 예약하는 API입니다.
requestAnimationFrame(() => {
// 다음 페인트 직전에 실행될 작업
});
setTimeout(fn, 16)처럼 시간을 대략 맞추는 방식과는 다릅니다. requestAnimationFrame은 실제 디스플레이 주사율과 브라우저의 프레임 스케줄링에 맞춰 실행됩니다. 그래서 애니메이션처럼 눈에 보이는 DOM 변경을 프레임 흐름에 맞추기에 적합합니다.
다만 rAF를 썼다고 자동으로 코드가 빨라지는 것은 아닙니다. 핵심은 DOM 작업의 순서입니다.
문제는 DOM 읽기와 쓰기를 섞는 패턴
브라우저는 화면을 그릴 때 대략 다음 단계를 거칩니다.
- 스타일 계산
- 레이아웃 계산
- 페인트
- 컴포지팅
JS에서 DOM 스타일을 바꾸면 브라우저는 관련 계산을 나중으로 미룰 수 있습니다. 그런데 스타일을 바꾼 직후 getBoundingClientRect(), offsetHeight, scrollTop 같은 값을 읽으면 이야기가 달라집니다.
브라우저는 정확한 값을 반환하기 위해 미뤄둔 스타일과 레이아웃 계산을 그 자리에서 끝내야 합니다. 이때 발생하는 것이 강제 동기 레이아웃입니다.
한 번이라면 큰 문제가 아닐 수 있습니다. 하지만 스크롤 핸들러, 드래그 UI, 리스트 렌더링처럼 반복되는 흐름에서 읽기와 쓰기가 계속 섞이면 layout thrashing으로 이어질 수 있습니다.
읽기 먼저, 쓰기 나중
실무에서는 DOM 작업을 다음처럼 나누는 습관이 중요합니다.
- 먼저 현재 레이아웃 값을 읽는다.
- 이후 화면 변경 작업을 예약한다.
- 가능하면
transform,opacity처럼 레이아웃 부담이 적은 속성을 사용한다.
예시는 다음과 같습니다.
<div id="box" style="width: 80px; height: 80px; background: tomato;"></div>
<button id="move">move</button>
<script>
const box = document.querySelector("#box");
const button = document.querySelector("#move");
button.addEventListener("click", () => {
// 읽기: 먼저 현재 레이아웃 값을 측정한다.
const currentLeft = box.getBoundingClientRect().left;
// 쓰기: 화면 반영은 다음 프레임 직전에 예약한다.
requestAnimationFrame(() => {
box.style.transform = `translateX(${currentLeft + 100}px)`;
});
});
</script>
여기서 중요한 점은 getBoundingClientRect()로 현재 위치를 먼저 읽고, 실제 화면 변경은 requestAnimationFrame 안에서 처리한다는 것입니다.
이렇게 하면 “측정”과 “반영”의 단계가 분리됩니다. 브라우저 입장에서도 레이아웃을 언제 계산해야 하는지 더 예측하기 쉬워집니다.
transform을 선호하는 이유
요소를 이동시킬 때 left, top, width, height 같은 속성을 바꾸면 레이아웃 계산이 다시 필요해질 수 있습니다. 반면 transform은 많은 경우 레이아웃 흐름 자체를 다시 계산하지 않고 합성 단계에서 처리될 수 있어 애니메이션에 유리합니다.
물론 모든 상황에서 transform이 정답은 아닙니다. 레이아웃 자체가 바뀌어야 하는 UI라면 width나 height 변경이 필요할 수 있습니다. 다만 단순 이동, 확대, 투명도 변화처럼 시각적 변화가 목적이라면 transform과 opacity를 먼저 고려하는 것이 좋습니다.
코드 리뷰에서 볼 부분
성능 문제를 찾을 때는 다음 패턴을 먼저 확인하면 좋습니다.
- 반복문 안에서
style.xxx변경과offsetHeight읽기가 섞여 있는가 - 스크롤 이벤트마다 DOM 측정과 DOM 변경이 동시에 일어나는가
- 드래그 중 매 프레임마다 레이아웃 속성을 바꾸고 있지는 않은가
- 이동 애니메이션에
left/top대신transform을 쓸 수 있는가
requestAnimationFrame은 성능을 자동으로 해결하는 마법 도구가 아닙니다. 하지만 DOM 읽기와 쓰기를 분리하는 구조와 함께 쓰면 프레임 드랍을 줄이는 데 도움이 됩니다.
마무리
requestAnimationFrame은 다음 페인트 직전에 시각적 변경을 예약하는 API입니다. 하지만 더 중요한 원칙은 DOM을 바꾼 직후 레이아웃 값을 읽는 일을 줄이는 것입니다.
프론트엔드 성능을 개선하고 싶다면 먼저 다음 문장을 기억하면 됩니다.
DOM은 읽기 먼저, 쓰기는 나중에.
이 원칙만 지켜도 스크롤, 드래그, 애니메이션 코드에서 불필요한 레이아웃 계산을 줄일 수 있습니다.
'JS' 카테고리의 다른 글
| CORS와 Preflight 요청, 헷갈리는 이유와 실무 디버깅법 (0) | 2026.07.26 |
|---|---|
| 브라우저 이벤트 루프: task, microtask, rAF 차이 (0) | 2026.07.23 |
