FEInterview Prep

browser · high priority

Observer APIs — Intersection · Resize · Mutation · Performance

*폴링과 이벤트의 한계* 를 넘는 *비동기 관찰자* 4 종 비교 가이드

intermediate 난이도4시간토스카카오네이버배민당근라인쿠팡
시작 전
이해도
매우 낮음

학습 개요

탄생 배경

쉬운 설명

복잡한 개념을 실생활 비유로 설명합니다.

*경비원에게 맡긴 감시*

폴링 (`setInterval`) 은 *내가 직접 5 초마다 창문에 나가 보는 일* 입니다. 누가 들어왔는지 매번 확인하느라 *내 일을 못 합니다*. 이벤트 (`scroll`) 는 *지나가는 사람마다 종이 울리는 시스템* 으로, *조용한 시간에도 종을 듣느라* 피곤합니다. Observer 는 *경비원에게 "수상한 변화가 생기면 한꺼번에 모아서 알려 주세요" 라고 부탁하는 일* 입니다. 경비원(브라우저) 은 *효율적인 자기 시점* 에 *모인 변화를 묶어* 알려 주고, 그동안 *나는 다른 일* 을 합니다. 4 종 Observer 는 *경비원이 무엇을 감시하느냐* 의 차이일 뿐 — 시야 진입 (Intersection), 가구 크기 변경 (Resize), 가구 자체의 변형 (Mutation), 건물 성능 지표 (Performance).

핵심 개념

같은 문제, *옛 방식* vs *Observer*

❌ scroll 이벤트 + getBoundingClientRect
  • 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});
✅ IntersectionObserver
  • 뷰포트 진입을 *브라우저가 직접* 통보
  • 콜백은 *진입/이탈 변화 시에만* 실행 — *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 개 진입* 일어났을 때 몇 번 호출되는가?

기대 키워드

1번배치entries.length 100

자주 하는 오해

*"진입 100 번 = 콜백 100 번"* — 비동기 + 배치라 *한 번* 에 묶임.

PerformanceObserver 가 *과거에 발생한 엔트리* 까지 받게 하는 옵션은?

기대 키워드

`buffered: true`

자주 하는 오해

*"등록 시점 이후만 받는다"* — 기본은 그렇지만 `buffered: true` 로 *과거도 포함*.

`ResizeObserver loop` 경고를 회피하는 *표준 패턴* 한 줄.

기대 키워드

`requestAnimationFrame`콜백 내 변형 미루기

자주 하는 오해

*"무조건 버그"* — 대부분 *기능 영향 없는 경고*. 콜백 단순화 또는 rAF.

학습 자료