DOM의 변화를 감지하는 Observer API는 세 가지가 있습니다. Intersection Observer(가시성), ResizeObserver(크기), MutationObserver(구조). 이번 글에서는 나머지 두 가지를 다룹니다.

ResizeObserver — 요소 크기 변화 감지

요소의 크기가 변경될 때 콜백을 실행합니다. window.resize 이벤트와 달리 개별 요소 의 크기 변화를 감지합니다.

JS
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

JS
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}`);
    }
  }
});

실전 — 반응형 컴포넌트

JS
// 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

JS
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";
  });
}

실전 — 차트 리사이징

JS
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 트리의 변경(자식 추가/제거, 속성 변경, 텍스트 변경)을 감지합니다.

JS
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();

실전 — 동적 스크립트 감지

JS
// 제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,
});

실전 — 다크모드 감지

JS
// 외부 라이브러리가 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 변경 대기

JS
// 특정 요소가 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");

성능 주의사항

JS
// 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요소 크기 변화반응형 컴포넌트, 차트
MutationObserverDOM 구조/속성 변화동적 DOM 감지, 플러그인

**기억하기 **: ResizeObserver는 window.resize 이벤트의 요소 단위 버전이고, MutationObserver는 DOM 변경의 이벤트 리스너입니다. 세 가지 Observer를 적절히 조합하면 폴링 없이 효율적으로 DOM 변화를 추적할 수 있습니다.

댓글 로딩 중...