ResizeObserver와 MutationObserver — DOM 변화 감지 API
DOM의 변화를 감지하는 Observer API는 세 가지가 있습니다. Intersection Observer(가시성), ResizeObserver(크기), MutationObserver(구조). 이번 글에서는 나머지 두 가지를 다룹니다.
ResizeObserver — 요소 크기 변화 감지
요소의 크기가 변경될 때 콜백을 실행합니다. window.resize 이벤트와 달리 개별 요소 의 크기 변화를 감지합니다.
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
console.log(`${entry.target.id}: ${width}x${height}`);
}
});
observer.observe(document.querySelector(".container"));
contentRect vs borderBoxSize
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
// contentRect — padding, border 제외한 콘텐츠 영역
const { width, height } = entry.contentRect;
// borderBoxSize — border + padding + content (더 정확)
if (entry.borderBoxSize) {
const [box] = entry.borderBoxSize;
console.log(`Border Box: ${box.inlineSize}x${box.blockSize}`);
}
// contentBoxSize — padding 제외
if (entry.contentBoxSize) {
const [box] = entry.contentBoxSize;
console.log(`Content Box: ${box.inlineSize}x${box.blockSize}`);
}
}
});
실전 — 반응형 컴포넌트
// CSS Container Query 없이 요소 크기에 따라 레이아웃 변경
function makeResponsive(element) {
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const width = entry.contentRect.width;
entry.target.classList.toggle("compact", width < 300);
entry.target.classList.toggle("medium", width >= 300 && width < 600);
entry.target.classList.toggle("wide", width >= 600);
}
});
observer.observe(element);
return () => observer.disconnect();
}
실전 — 자동 높이 textarea
function autoResize(textarea) {
const observer = new ResizeObserver(() => {
textarea.style.height = "auto";
textarea.style.height = textarea.scrollHeight + "px";
});
observer.observe(textarea);
textarea.addEventListener("input", () => {
textarea.style.height = "auto";
textarea.style.height = textarea.scrollHeight + "px";
});
}
실전 — 차트 리사이징
class ResponsiveChart {
constructor(container) {
this.container = container;
this.canvas = container.querySelector("canvas");
this.resizeObserver = new ResizeObserver((entries) => {
const { width, height } = entries[0].contentRect;
this.canvas.width = width * devicePixelRatio;
this.canvas.height = height * devicePixelRatio;
this.canvas.style.width = `${width}px`;
this.canvas.style.height = `${height}px`;
this.redraw();
});
this.resizeObserver.observe(container);
}
destroy() {
this.resizeObserver.disconnect();
}
}
MutationObserver — DOM 구조 변화 감지
DOM 트리의 변경(자식 추가/제거, 속성 변경, 텍스트 변경)을 감지합니다.
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
switch (mutation.type) {
case "childList":
console.log("자식 노드 변경:", mutation.addedNodes, mutation.removedNodes);
break;
case "attributes":
console.log(`속성 변경: ${mutation.attributeName} = ${mutation.target.getAttribute(mutation.attributeName)}`);
break;
case "characterData":
console.log("텍스트 변경:", mutation.target.data);
break;
}
}
});
observer.observe(document.querySelector("#app"), {
childList: true, // 자식 노드 추가/제거
attributes: true, // 속성 변경
characterData: true, // 텍스트 변경
subtree: true, // 하위 트리 전체 감시
attributeOldValue: true, // 변경 전 속성값 기록
characterDataOldValue: true, // 변경 전 텍스트 기록
attributeFilter: ["class", "style"], // 특정 속성만 감시
});
// 감시 중지
observer.disconnect();
// 보류 중인 기록 가져오기
const pending = observer.takeRecords();
실전 — 동적 스크립트 감지
// 제3자 스크립트가 DOM에 요소를 추가하는 것 감지
const scriptObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.tagName === "SCRIPT") {
console.warn("새 스크립트 삽입 감지:", node.src);
}
if (node.tagName === "IFRAME") {
console.warn("새 iframe 삽입 감지:", node.src);
}
}
}
});
scriptObserver.observe(document.documentElement, {
childList: true,
subtree: true,
});
실전 — 다크모드 감지
// 외부 라이브러리가 body에 class를 추가하는 것 감지
const themeObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.attributeName === "class") {
const isDark = document.body.classList.contains("dark-mode");
updateTheme(isDark);
}
}
});
themeObserver.observe(document.body, {
attributes: true,
attributeFilter: ["class"],
});
실전 — DOM 변경 대기
// 특정 요소가 DOM에 나타날 때까지 기다리기
function waitForElement(selector, timeout = 5000) {
return new Promise((resolve, reject) => {
// 이미 존재하면 바로 반환
const existing = document.querySelector(selector);
if (existing) {
resolve(existing);
return;
}
const observer = new MutationObserver((mutations, obs) => {
const element = document.querySelector(selector);
if (element) {
obs.disconnect();
resolve(element);
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
// 타임아웃
setTimeout(() => {
observer.disconnect();
reject(new Error(`${selector}를 찾을 수 없습니다.`));
}, timeout);
});
}
// 사용
const modal = await waitForElement(".modal-content");
성능 주의사항
// MutationObserver 콜백은 마이크로태스크로 실행됨
// 대량 DOM 변경 시 한 번에 모아서 콜백 실행
// 나쁜 예: 콜백에서 DOM을 다시 변경 → 무한 루프 위험
const observer = new MutationObserver((mutations) => {
// 주의: 여기서 DOM을 변경하면 다시 콜백이 호출될 수 있음
element.textContent = "변경"; // 재귀 호출 위험!
});
// 좋은 예: 필요할 때만 변경, 가드 조건 추가
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.target.textContent !== "원하는 값") {
observer.disconnect(); // 잠시 중지
mutation.target.textContent = "원하는 값";
observer.observe(element, { characterData: true }); // 재시작
}
}
});
Observer API 비교
| API | 감지 대상 | 주요 용도 |
|---|---|---|
| IntersectionObserver | 가시성 (뷰포트 교차) | 무한 스크롤, Lazy Loading |
| ResizeObserver | 요소 크기 변화 | 반응형 컴포넌트, 차트 |
| MutationObserver | DOM 구조/속성 변화 | 동적 DOM 감지, 플러그인 |
**기억하기 **: ResizeObserver는 window.resize 이벤트의 요소 단위 버전이고, MutationObserver는 DOM 변경의 이벤트 리스너입니다. 세 가지 Observer를 적절히 조합하면 폴링 없이 효율적으로 DOM 변화를 추적할 수 있습니다.
댓글 로딩 중...