웹 애플리케이션을 개발하면서 특정 컴포넌트나 차트의 크기가 바뀔 때 레이아웃을 다시 그리려다 화면이 순간적으로 멈칫하는 현상을 경험해봤을 것이다. 다만 정확한 원인이 window 객체의 onresize 이벤트가 유발하는 레이아웃 리플로우(Reflow)와 과호출 때문이라는 점을 모른 채 무작정 디바운스(Debounce) 코드만 붙이는 경우가 많다. 이번에는 ResizeObserver API가 정확히 무엇인지, 왜 기존 window 기반 감지보다 훨씬 효율적인지, 실무 코드에 어떻게 적용하는지 완벽하게 정리해서 소개하겠다.
과거에는 특정 div 요소의 크기 변화를 직접 감지하는 브라우저 표준 API가 없었다. 그래서 대부분의 개발자들은 브라우저 창 전체의 크기가 변경될 때 발생하는 window resize 이벤트를 수신한 뒤, 감지하고 싶은 요소의 offsetWidth나 getBoundingClientRect()를 매번 직접 조회하여 변경 여부를 비교했다.
하지만 이 방식은 두 가지 결정적인 실무 문제를 발생시킨다. 첫째, 브라우저 창 크기가 바뀌지 않고 사이드바가 열리거나 동적으로 텍스트가 들어가서 target 요소만 크기가 변하는 경우 window resize 이벤트는 아예 동작하지 않는다. 둘째, 요소의 크기를 획득하기 위해 DOM 레이아웃 속성을 읽는 순간 브라우저는 동기적으로 레이아웃 재계산(Reflow)을 강제 실행하므로 극심한 프레임 드랍이 일어난다.
ResizeObserver API는 이러한 한계를 완전히 극복한다. 개별 DOM 요소의 관찰 대상(Target)을 지정하면 브라우저의 Render 파이프라인 내부에서 스타일 재계산이 완료된 직후 비동기로 크기 변경 통지를 전달받는다. 불필요한 레이아웃 강제 재계산 없이 정확히 관찰 대상 요소의 변경 시점만 포착할 수 있다.
ResizeObserver의 사용 방법은 IntersectionObserver나 MutationObserver와 매우 유사하다. 인스턴스를 생성할 때 콜백 함수를 전달하고, observe() 메서드로 관찰할 대상 DOM 요소를 지정하면 된다.
| 구분 | window.onresize | ResizeObserver API |
|---|---|---|
| 감지 범위 | 브라우저 뷰포트(Window) 전체 | 개별 DOM 요소 (특정 div, container 등) |
| 감지 조건 | 창 크기 조절 시에만 발생 | 요소의 padding, content box 크기 변경 시 통지 |
| 성능 영향 | 동기적 Reflow 유발, 이벤트 과호출 위험 | 비동기 렌더링 파이프라인 연동, 성능 최적화됨 |
| 사이드바/동적 layout 감지 | 불가능 (별도 윈도우 리사이즈 없음) | 완벽 감지 가능 |
반응형 대시보드에서 차트 canvas나 svg가 부모 요소의 너비에 맞춰 다시 그려져야 하는 상황을 가정해보자.
✗ 잘못된 코드: window.onresize와 getBoundingClientRect 연동
이 코드는 창 전체가 휠 때만 반응하며, 마우스 드래그 하나에 초당 수십 번의 레이아웃 재계산이 발생하여 스크롤 및 렌더링 성능을 심각하게 떨어뜨린다.
// ✗ 잘못된 방식: 브라우저 창 전체 이벤트를 구독하여 요소 크기를 매번 강제 측정
const chartElement = document.getElementById("myChart");
window.addEventListener("resize", () => {
// 레이아웃 강제 재계산(Reflow) 발생
const width = chartElement.offsetWidth;
const height = chartElement.offsetHeight;
console.log("Window Resize 감지 - 차트 크기 recalculate:", width, height);
renderChart(width, height);
});
✓ 올바른 코드: ResizeObserver를 활용한 관찰 및 requestAnimationFrame 조합
ResizeObserver를 활용하면 요소 자체의 border-box 또는 content-box 크기를 정확하고 안전하게 읽어올 수 있다. 관찰이 끝난 후에는 반드시 unobserve나 disconnect를 호출하여 메모리 누수를 방지해야 한다.
// ✓ 올바른 방식: ResizeObserver API로 특정 요소만 비동기 관찰
const chartElement = document.getElementById("myChart");
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
// entry.contentRect 또는 entry.borderBoxSize 사용 가능
const { width, height } = entry.contentRect;
// 렌더링 프레임에 맞추어 차트 업데이트 실행
window.requestAnimationFrame(() => {
console.log("요소 크기 변경 완료:", width, height);
renderChart(width, height);
});
}
});
// 대상 요소 관찰 시작
observer.observe(chartElement);
// 컴포넌트 해제 시 메모리 정리를 위한 분리 로직
function cleanup() {
observer.unobserve(chartElement);
observer.disconnect();
}
ResizeObserver를 사용할 때 콘솔에 ResizeObserver loop limit exceeded 경고 메시지가 뜨는 경우가 있다. 이는 콜백 함수 내부에서 관찰 대상 요소의 크기를 다시 변경하는 스타일(예: width, padding 변경 등)을 동기적으로 수정할 때 발생한다. 브라우저는 무한 루프에 빠지는 것을 막기 위해 해당 프레임의 통지를 중단하고 이 경고를 발생시킨다.
✗ 잘못된 예: 콜백 내부에서 관찰 대상의 inline style 너비를 직접 변경함
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
// ✗ 관찰 대상의 크기를 콜백 내에서 직접 변경하면 무한 재귀 및 루프 에러 발생
entry.target.style.width = (entry.contentRect.width + 10) + "px";
}
});✓ 올바른 예: 크기 변경으로 인한 스타일 재계산이 필요한 경우 requestAnimationFrame으로 다음 프레임에 지연 처리하거나 캔버스 내부 드로잉 루틴만 변경해야 한다.
기존 window.onresize에 의존하던 반응형 인터랙션 관리는 프레임 드랍과 비효율적인 리플로우를 초래하는 주범이다. 요소 단위의 유연한 반응형 UI 및 차트 자동 리사이징을 구현할 때는 표준 ResizeObserver API를 도입하는 것이 성능 최적화의 첫걸음이다.
ResizeObserver는 프론트엔드 최적화에서 매우 중요한 도구다. 작은 최적화와 올바른 이벤트 관찰 습관이 모여서 사용자 편의성과 매끄러운 60fps 렌더링이라는 큰 효과를 만든다는 점을 잊지 말자. 이 글의 실전 예제 코드를 참고해 기존 window resize 이벤트 수신기들을 ResizeObserver로 전환하면, 즉각적인 화면 렌더링 성능 향상이라는 최종 결과를 얻을 수 있을 것이다.