browser · high priority
Observer APIs — Intersection · Resize · Mutation · Performance
*폴링과 이벤트의 한계* 를 넘는 *비동기 관찰자* 4 종 비교 가이드
학습 개요
탄생 배경
쉬운 설명
복잡한 개념을 실생활 비유로 설명합니다.
“*경비원에게 맡긴 감시*”
폴링 (`setInterval`) 은 *내가 직접 5 초마다 창문에 나가 보는 일* 입니다. 누가 들어왔는지 매번 확인하느라 *내 일을 못 합니다*. 이벤트 (`scroll`) 는 *지나가는 사람마다 종이 울리는 시스템* 으로, *조용한 시간에도 종을 듣느라* 피곤합니다. Observer 는 *경비원에게 "수상한 변화가 생기면 한꺼번에 모아서 알려 주세요" 라고 부탁하는 일* 입니다. 경비원(브라우저) 은 *효율적인 자기 시점* 에 *모인 변화를 묶어* 알려 주고, 그동안 *나는 다른 일* 을 합니다. 4 종 Observer 는 *경비원이 무엇을 감시하느냐* 의 차이일 뿐 — 시야 진입 (Intersection), 가구 크기 변경 (Resize), 가구 자체의 변형 (Mutation), 건물 성능 지표 (Performance).
핵심 개념
같은 문제, *옛 방식* vs *Observer*
- scroll 이 *초당 수십~수백 번* 발화
- 매번
getBoundingClientRect()가 *forced reflow* 유발 - throttle/debounce 없이는 60fps 붕괴
- *"보이는가?"* 의 정답을 *우리가 매번 재계산*
1window.addEventListener('scroll', () => {2 const rect = el.getBoundingClientRect();3 if (rect.top < window.innerHeight) {4 loadImage(el);5 }6});
- 뷰포트 진입을 *브라우저가 직접* 통보
- 콜백은 *진입/이탈 변화 시에만* 실행 — *idle 시간* 활용
- *forced reflow 없음* — 합성기 레벨에서 계산
- *"보이는가?"* 의 정답을 *브라우저가 들고 옴*
1const io = new IntersectionObserver((entries) => {2 entries.forEach((e) => {3 if (e.isIntersecting) loadImage(e.target);4 });5});6io.observe(el);
Observer 패턴의 *공통 API*
new XxxObserver(callback) → observe(target) → unobserve(target) → disconnect(). takeRecords() 로 *대기 중 엔트리 수동 회수*.
비동기 콜백 시점
*Intersection / Resize* 는 *애니메이션 프레임 직전* 에, *Mutation* 은 *마이크로태스크* 로, *Performance* 는 *엔트리 발생 시 큐* 에 쌓아 묶어서 호출. *동기 이벤트가 아니므로* 콜백 안에서 동기 DOM 측정은 위험.
*배치 (batching)*
여러 변경이 *한 콜백* 으로 묶여 전달된다. 한 프레임에 100 개 이미지가 진입해도 콜백은 *한 번* + entries 배열 길이 100.
*콜백 안에서 다시 측정/변형* 하면 위험
Resize 콜백 안에서 DOM 의 폭을 다시 바꾸면 *또 다른 resize* 가 트리거된다. 한 프레임 안에서 처리되지 못하면 브라우저가 ResizeObserver loop completed with undelivered notifications. 오류를 던지고 *나머지 통보를 다음 프레임으로 미룬다*. 무한 루프는 막히지만 *경고는 모니터링에 잡힘* — 콜백 안의 변형은 *requestAnimationFrame* 에 미루는 패턴이 안전.
실무 적용
어떤 상황에서 사용하는가
커머스 메인 페이지 — *상품 카드 무한 스크롤*, *섹션 진입 시 임프레션 측정*, *이미지 lazy load*, *상단 캐러셀의 폭에 맞춰 슬라이드 개수 변경*, *Web Vitals 의 LCP/CLS/INP 를 RUM 서버로 전송*.
어떻게 적용하는가
(1) *무한 스크롤* 은 리스트 끝에 *sentinel div* 를 두고 `IntersectionObserver` 로 진입 시 다음 페이지를 prefetch (`rootMargin: "400px"` 로 미리 로드). (2) *임프레션 측정* 은 `threshold: 0.5` 로 *50% 보였을 때만* 1회 발화하고 `unobserve` (광고 정책 흔히 50%/1초 규칙). (3) *이미지 lazy load* 는 `<img loading="lazy">` 로 충분한 경우가 대부분이지만, *진입 시 placeholder 페이드인* 같은 커스텀이 필요하면 IO 직접 사용. (4) *캐러셀 폭 반응* 은 `ResizeObserver` 로 컨테이너 inlineSize 를 보고 슬라이드 개수를 다시 계산 — *콜백 안에서 DOM 변형* 이 필요하면 `requestAnimationFrame` 으로 미뤄 *RO loop* 경고 회피. (5) *Web Vitals* 는 `web-vitals` 라이브러리로 받아 `navigator.sendBeacon` 으로 RUM 엔드포인트에 전송 — `PerformanceObserver` 직접 구현 대비 BFCache 등 엣지 케이스가 *이미 처리* 되어 있어 안전. (6) 모든 Observer 는 컴포넌트 unmount 시 `disconnect()` 로 정리하고, React 라면 `useEffect` 의 cleanup 함수에서 호출.
흔한 실수와 안티패턴
- `disconnect()` 를 잊어 *언마운트 후에도 콜백이 발화* — 메모리 누수 + setState on unmounted.
- Intersection 의 `rootMargin` 단위에 *`0` 만 쓰는 실수* — 반드시 `"0px"` 처럼 단위.
- Resize 콜백 안에서 *동기 DOM 변형* → `ResizeObserver loop` 경고. rAF 로 미루기.
- Mutation `subtree: true` 를 깊은 트리에 걸어 *과도한 콜백* 발화.
- Performance 의 `buffered: true` 를 잊어 *페이지 로드 직후 발생한 LCP* 를 놓침.
- `new IntersectionObserver(cb, { threshold: [0, 0.25, 0.5, 1] })` 의 *콜백 빈도* 가 4 배가 되는 점을 잊고 *무거운 작업* 을 둠.
흔한 오해
*"Observer 콜백은 동기적으로 실행된다"*
교정*비동기 + 배치* 다. 한 프레임에 100 개 진입이 일어나도 콜백은 *한 번 + entries 100 개* 다. 콜백 안에서 *getBoundingClientRect 같은 동기 측정* 은 *그 시점의 레이아웃* 이 안정인지 보장이 없다.
왜 중요브라우저가 *프레임 직전* 또는 *마이크로태스크* 시점에 묶어서 호출하기 때문 — 메인 스레드 비용 최적화의 핵심.
*"`unobserve` 와 `disconnect` 는 같다"*
교정`unobserve(target)` 은 *그 한 대상만 해제*, `disconnect()` 는 *모든 관찰 대상* 해제. 컴포넌트 언마운트엔 `disconnect`, 한 번 발화 후 종료엔 `unobserve` 가 표준.
왜 중요재사용 가능한 단일 Observer 로 여러 요소를 관찰하는 경우가 흔하기 때문.
*"`ResizeObserver loop` 에러는 무조건 버그"*
교정대부분의 경우 *경고* 이고 기능엔 영향 없다. 콜백 안에서 *resize 를 다시 유발하는 변형* 이 한 프레임에 마무리되지 못해 *다음 프레임으로 미뤄지는* 현상. 콜백을 단순화하거나 *rAF 안에 넣으면* 사라진다.
왜 중요브라우저가 *무한 루프 방지* 를 위해 의도적으로 통보를 미루는 *방어 메커니즘* 이고 그 신호가 콘솔에 노출될 뿐.
*"Mutation Events 와 MutationObserver 는 거의 같다"*
교정*Mutation Events 는 deprecated*. 동기 + 매 변형마다 + 버블링까지 발생해 *깊은 트리에서 폭발적* 으로 무거웠다. MutationObserver 는 *비동기 + 배치* 로 그 문제를 근본적으로 해결.
왜 중요같은 목적의 *세대 차이 큰* API. 새 코드에선 Mutation Events 를 절대 쓰지 않는다.
면접 질문
답변 방향 힌트
"forced reflow", "프리로드", "disconnect on unmount".
반드시 언급할 키워드
- scroll 이벤트는 *고빈도 + getBoundingClientRect → forced reflow*
- IO 는 *비동기 + 배치* — 메인 스레드 비용 예측 가능
- sentinel div 패턴 — 리스트 *끝* 에 보이지 않는 div, IO 가 그 진입을 감지
- `rootMargin` 으로 *화면 밖에서 미리* 발화 (프리로드)
- `threshold` 는 보통 0 (살짝만 보여도 트리거)
- 컴포넌트 unmount 시 `disconnect()` 필수
- next page fetch 가 *동시에 여러 번* 호출되지 않도록 *로딩 락*
예상 꼬리 질문
- `rootMargin` 을 *너무 크게* 설정하면 어떤 부작용이 있나요?
- Virtualization (react-window, virtuoso) 과 *무한 스크롤* 은 어떻게 다른가요?
자기 점검
4 가지 Observer 의 *관찰 대상* 을 한 단어씩 답하라.
기대 키워드
자주 하는 오해
*"모두 비슷하다"* — 관찰 대상이 *근본적으로* 다르므로 대체 관계가 아니다.
IntersectionObserver 콜백이 *한 프레임에 100 개 진입* 일어났을 때 몇 번 호출되는가?
기대 키워드
자주 하는 오해
*"진입 100 번 = 콜백 100 번"* — 비동기 + 배치라 *한 번* 에 묶임.
PerformanceObserver 가 *과거에 발생한 엔트리* 까지 받게 하는 옵션은?
기대 키워드
자주 하는 오해
*"등록 시점 이후만 받는다"* — 기본은 그렇지만 `buffered: true` 로 *과거도 포함*.
`ResizeObserver loop` 경고를 회피하는 *표준 패턴* 한 줄.
기대 키워드
자주 하는 오해
*"무조건 버그"* — 대부분 *기능 영향 없는 경고*. 콜백 단순화 또는 rAF.
학습 자료
- MDN — IntersectionObserverIO 의 옵션·엔트리·예제 *정본* 문서.DocMDN
- MDN — ResizeObserverRO 와 4 가지 측정값 (`contentRect`/`contentBoxSize`/...).DocMDN
- MDN — MutationObserverMO 옵션 (`childList`/`subtree`/...) 과 마이크로태스크 모델.DocMDN
- MDN — PerformanceObserverWeb Vitals 측정의 표준 인터페이스.DocMDN
- web-vitals 라이브러리PerformanceObserver 위에 *올바른 측정 규약* 이 구현된 표준 라이브러리.CodeGoogle Chrome
- A Better API for the Resize ObserverRO 의 실전 패턴과 함정 (loop 경고 등).BlogCSS-Tricks