MutationObserver — DOM 변화 감지

MutationObserver로 DOM의 자식 노드 추가·제거, 속성 변경, 텍스트 변경을 비동기적으로 감지하는 방법과 MutationRecord 구조, 올바른 정리(disconnect) 패턴을 정리합니다.

· 5 min read · PALDYN Team

지난 글에서 CSSOM으로 스타일을 읽고 쓰는 방법을 정리했습니다. 이번에는 DOM 트리의 변화를 비동기적으로 감지하는 MutationObserver를 다룹니다.


왜 MutationObserver인가

과거에는 DOMNodeInserted, DOMAttrModified 같은 Mutation Events로 DOM 변화를 감지했습니다. 하지만 이 이벤트들은 변경마다 동기적으로 발화해 성능이 나빴고, 현재는 폐기됐습니다. MutationObserver는 변경 사항을 일괄 수집한 뒤 현재 태스크가 끝나는 시점에 마이크로태스크로 한 번에 전달합니다. 훨씬 효율적입니다.

MutationObserver 동작 흐름


기본 사용법

const observer = new MutationObserver((mutations, obs) => {
  for (const m of mutations) {
    console.log(m.type, m.target);
  }
});

observer.observe(document.querySelector('#container'), {
  childList: true,   // 자식 노드 추가/제거
  attributes: true,  // 속성 변경
  subtree: true,     // 모든 후손 포함
  characterData: true, // 텍스트 노드 내용 변경
  attributeOldValue: true, // 변경 전 속성값 기록
  characterDataOldValue: true,
});

// 감시 중단
observer.disconnect();

observe는 여러 대상에 호출할 수 있습니다. disconnect는 이 Observer가 감시하는 모든 대상을 중단합니다.

MutationObserver 코드 예제


MutationRecord 구조

콜백의 첫 번째 인자는 MutationRecord 배열입니다.

필드설명
type"childList" | "attributes" | "characterData"
target변경이 일어난 노드
addedNodes추가된 NodeList
removedNodes제거된 NodeList
attributeName변경된 속성 이름 (type이 attributes일 때)
oldValue변경 전 값 (옵션 활성화 시)
previousSibling / nextSibling추가/제거된 노드의 이전/다음 형제
const observer = new MutationObserver(mutations => {
  for (const { type, target, addedNodes, attributeName, oldValue } of mutations) {
    if (type === 'childList') {
      addedNodes.forEach(node => {
        if (node.nodeType === Node.ELEMENT_NODE) {
          console.log('요소 추가:', node.tagName);
        }
      });
    } else if (type === 'attributes') {
      console.log(`${attributeName}: ${oldValue} → ${target.getAttribute(attributeName)}`);
    }
  }
});

takeRecords — 미전달 레코드 즉시 수집

disconnect 전에 아직 전달되지 않은 레코드를 즉시 가져올 수 있습니다.

// 정리 전 남은 레코드 처리
const pending = observer.takeRecords();
observer.disconnect();
pending.forEach(m => process(m));

takeRecords를 호출하면 내부 큐가 비워집니다.


실전 패턴: 서드파티 DOM 변경 추적

프레임워크가 관리하는 DOM을 직접 건드리지 않고 반응해야 할 때 유용합니다.

function watchForElement(selector, callback) {
  // 이미 존재하면 바로 실행
  const existing = document.querySelector(selector);
  if (existing) { callback(existing); return; }

  const obs = new MutationObserver((mutations, self) => {
    for (const { addedNodes } of mutations) {
      for (const node of addedNodes) {
        if (node.nodeType !== Node.ELEMENT_NODE) continue;
        const el = node.matches(selector)
          ? node
          : node.querySelector(selector);
        if (el) {
          callback(el);
          self.disconnect(); // 한 번 찾으면 중단
          return;
        }
      }
    }
  });

  obs.observe(document.body, { childList: true, subtree: true });
}

주의 사항

무한 루프: 콜백 안에서 감시 대상 DOM을 변경하면 새 MutationRecord가 생성되어 콜백이 재진입합니다. 가드 조건을 두거나, 콜백 내에서 일시적으로 disconnect → 처리 → observe를 반복합니다.

메모리: Observer가 살아있는 한 target 노드가 GC되지 않습니다. 필요 없어지면 반드시 disconnect를 호출합니다. 컴포넌트 정리(cleanup, useEffect 반환값 등)에서 호출하는 것이 패턴입니다.

subtree + childList 비용: 대형 서브트리를 감시하면 변경이 잦을 때 콜백 호출 횟수가 많아집니다. 필요한 최소 범위만 감시합니다.


MutationObserver는 “내가 모르는 DOM 변경에 반응해야 할 때” 쓰는 도구입니다. 직접 제어하는 코드라면 변경 코드 뒤에 로직을 놓는 것이 훨씬 명확합니다.


지난 글: style · CSSOM — 자바스크립트로 스타일 다루기

다음 글: ResizeObserver · IntersectionObserver — 요소 크기와 가시성 감지


읽어주셔서 감사합니다. 😊