웹 애플리케이션을 개발하다 보면 외부 3rd-party 라이브러리가 생성한 DOM 요소나, 사용자 동작에 따라 동적으로 추가·변경되는 HTML 구조를 실시간으로 감지해야 하는 상황을 경험해봤을 것이다.
다만 대부분의 개발자들은 setInterval을 돌려 주기적으로 DOM을 검색하거나, 이미 구시대 유물이 된 MutationEvents(DOMNodeInserted 등)를 사용하다가 브라우저 성능 저하와 메모리 누수를 겪는 경우가 많다.
이번에는 Modern JavaScript의 MutationObserver API가 정확히 무엇이고, 왜 필요하며, 실무에서 어떻게 메모리 누수 없이 안전하게 활용하는지 완벽하게 정리해서 소개하겠다.
MutationObserver는 DOM 트리(DOM Tree)의 변화를 감지하여 비동기 콜백 함수를 실행해 주는 웹 표준 API다.
과거에는 DOM이 변경될 때마다 동기적으로 이벤트를 발생시키는 MutationEvents를 사용했으나, 이는 심각한 성능 저하를 유발하여 현재는 완전 폐기(Deprecated)되었다.
또한 setInterval을 활용한 폴링(Polling) 방식은 불필요한 CPU 자원을 계속 소비한다는 명확한 한계가 있다.
MutationObserver는 DOM 변화를 모아서 Microtask Queue를 통해 비동기로 처리하므로 웹 성능을 크게 개선해 준다.
| 비교 항목 | setInterval (폴링) | DOMNodeInserted (MutationEvents) | MutationObserver |
|---|---|---|---|
| 동작 방식 | 주기적 타이머 탐색 | DOM 변경 시 동기적 이벤트 발생 | DOM 변경을 모아 비동기 배치 처리 |
| 성능 영향 | 변화가 없어도 지속적 자원 소모 | 대량 DOM 변경 시 브라우저 렉 유발 | 최소한의 리소스 소비로 최적화됨 |
| 권장 여부 | ✗ 비권장 (임시 방편) | ✗ 사용 금지 (Deprecated) | ✓ 권장 (웹 표준 기술) |
MutationObserver를 사용할 때 가장 중요한 요소는 observer.observe(target, config) 메서드에 전달하는 옵션 객체다.
원하는 변화만 정확히 지정하지 않고 전체를 감시하면 불필요한 콜백이 남발될 수 있으므로 각 옵션의 역할을 명확히 알아 두어야 한다.
- childList: 자식 노드의 추가 또는 제거를 감지합니다. (boolean)
- attributes: 대상 노드의 속성(attribute) 변경을 감지합니다. (boolean)
- characterData: 노드의 텍스트 내용 변경을 감지합니다. (boolean)
- subtree: 대상 노드뿐만 아니라 모든 자식, 손자 노드까지 확장하여 감지합니다. (boolean)
- attributeOldValue: 속성이 변경될 때 변경 전 이전 값을 기록합니다. (boolean)
- characterDataOldValue: 텍스트가 변경될 때 변경 전 이전 값을 기록합니다. (boolean)
- attributeFilter: 특정 속성명만 골라서 감시할 때 배열로 지정합니다. (e.g. ['class', 'src'])
실무에서 자주 접하는 예시로 특정 컨테이너 안에 동적으로 리스트 아이템이 추가될 때 이를 실시간으로 감지하여 로직을 처리하는 코드를 비교해 보자.
다만 대부분의 초급 개발자들은 아래처럼 주기적으로 document.querySelector를 호출하는 방식을 흔히 작성한다.
// ✗ 비효율적: DOM 변화 여부와 상관없이 0.5초마다 계속 DOM을 검색함
const checkInterval = setInterval(() => {
const targetElement = document.querySelector('#dynamic-list .new-item');
if (targetElement) {
console.log('새 요소 발견:', targetElement.textContent);
// 한 번 찾은 후 정지하지 않으면 계속 메모리와 CPU를 점유함
}
}, 500);이 방식은 감지 대상 요소가 없거나 변화가 없는 동안에도 브라우저 스레드를 쓸데없이 점유하게 된다.
MutationObserver를 사용하면 실제로 DOM 변화가 일어난 시점에만 콜백이 실행되며, 작업이 끝난 후 disconnect()를 호출하여 메모리 누수를 완벽히 방지할 수 있다.
// 1. 감시할 대상 요소선택
const targetContainer = document.getElementById('dynamic-list');
// 2. 콜백 함수 정의 (변화가 발생했을 때 실행)
const callback = (mutationsList, observer) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
// 요소 노드(Node.ELEMENT_NODE)인 경우만 처리
if (node.nodeType === Node.ELEMENT_NODE) {
console.log('✓ 새 노드 추가 감지:', node.textContent);
}
});
}
}
};
// 3. 옵저버 인스턴스 생성 및 옵션 설정
const observer = new MutationObserver(callback);
const config = { childList: true, subtree: true };
// 4. 관찰 시작
if (targetContainer) {
observer.observe(targetContainer, config);
}
// 5. 더 이상 감시할 필요가 없거나 컴포넌트가 언마운트될 때 해제
// observer.disconnect();실행 결과 / 출력값:
HTML 컨테이너에 <li class="item">새로운 데이터</li>가 추가되는 순간 콘솔에 ✓ 새 노드 추가 감지: 새로운 데이터 가 즉시 출력되며, 불필요한 반복 조회 연산이 전혀 발생하지 않는다.
MutationObserver를 사용할 때 가장 주의해야 할 실수는 **콜백 함수 내부에서 다시 관찰 대상 DOM을 변경하는 것**이다. 이 경우 Observer가 자신의 변경 사항을 다시 감지하여 무한 루프(Infinite Loop)에 빠지게 된다.
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
// ✗ 위험: class 속성 변경을 감지하면서 콜백 내부에서 다시 class를 변경함
if (mutation.attributeName === 'class') {
mutation.target.classList.add('processed'); // 다시 Mutation이 발생하여 콜백 재호출!
}
});
});
observer.observe(document.getElementById('box'), { attributes: true });
const targetBox = document.getElementById('box');
const observer = new MutationObserver((mutations, obs) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'class') {
// 이미 처리된 경우 실행되지 않도록 방어 코드 추가
if (mutation.target.classList.contains('processed')) return;
// 필요 시 잠시 감시를 중단하고 수정 후 다시 연결
obs.disconnect();
mutation.target.classList.add('processed');
console.log('✓ 안전하게 클래스 추가 완료');
// 수정 완료 후 다시 관찰 재개
obs.observe(targetBox, { attributes: true });
}
});
});
if (targetBox) {
observer.observe(targetBox, { attributes: true });
}
MutationObserver API는 동적 웹 애플리케이션에서 안전하고 효율적으로 DOM 변경을 감지하는 강력한 도구다. 메모리 관리와 옵션 최적화라는 작은 습관이 모여서 웹 서비스의 전반적인 반응성과 안정성을 크게 높여준다는 점을 잊지 말자. 이 글의 실전 코드와 주의사항을 참고해 기존의 폴링 로직을 MutationObserver로 개선하면, 불필요한 리소스 소모 없이 깔끔한 이벤트 기반 웹 애플리케이션을 구축할 수 있을 것이다.