Home
PostsAbout
javascript

옵저버 옵저빙하기

JavaScript가 제공하는 여러 Observer 인터페이스에 대해 알아보자

2024-10-11

MutationObserver

MutationObserver 인터페이스는 DOM 트리의 변경을 감지하는 기능을 제공합니다. (by MDN)

MutationObserver는 DOM의 변화를 감지하고 이를 비동기적으로 처리하기 위한 웹 API입니다. 특정 요소의 속성, 자식 노드, 텍스트 내용 등의 변경 사항을 관찰하고 콜백 함수를 통해 해당 변화를 감지할 수 있습니다.

예제 코드

// MutationObserver 생성 및 콜백 함수 정의
const observer = new MutationObserver((mutationsList, observer) => {
  mutationsList.forEach((mutation) => {
    console.log('Mutation detected:', mutation);
  });
});

// 감지하려는 대상 요소 선택
const targetNode = document.getElementById('myElement');

// 감지 옵션 설정
const config = {
  childList: true,    // 자식 노드의 추가/제거 감지
  attributes: true,   // 속성 변경 감지
  subtree: true,      // 하위 노드까지 감지
  characterData: true // 텍스트 내용 변경 감지
};

// 감지 시작
observer.observe(targetNode, config);

// 감지 중지
// observer.disconnect();

MutationObserver는 왜 비동기적으로 처리할까요?

MutationObserver는 DOM에 변화가 있을 때마다 즉시 콜백 함수를 실행하지 않습니다. 대신, DOM 변화가 발생하면 현재 실행 중인 작업이 완료된 후, 다음 이벤트 루프 사이클에서 콜백 함수가 실행됩니다. 이를 통해 DOM 업데이트와 콜백 함수 실행 사이의 과도한 연산을 방지하고, 브라우저의 성능을 최적화할 수 있습니다.

MutationObserver가 감지할 수 있는 DOM 변화에는 어떤 것이 있을까요?

다음은 MutationObserver가 감지할 수 있는 주요 항목입니다.

  1. 자식 노드(childList) 변경

    • 요소의 자식 노드가 추가되거나 제거될 때 변화를 감지합니다.
    const observer = new MutationObserver((mutationsList) => {
      mutationsList.forEach((mutation) => {
        if (mutation.type === 'childList') {
          console.log('자식 노드가 추가 또는 제거되었습니다.');
        }
      });
    });
    
    observer.observe(targetElement, { childList: true });
    
  2. 속성(attributes) 변경

    • 요소의 속성(attribute)이 변경되면 이를 감지합니다.
    • id, class, style, data-* 등이 있겠죠.
    const observer = new MutationObserver((mutationsList) => {
      mutationsList.forEach((mutation) => {
        if (mutation.type === 'attributes') {
          console.log(`속성 '${mutation.attributeName}'이 변경되었습니다.`);
          if (mutation.oldValue) {
            console.log(`이전 값: ${mutation.oldValue}`);
          }
        }
      });
    });
    
    observer.observe(targetElement, { attributes: true, attributeOldValue: true });
    
  3. 텍스트 내용(characterData) 변경

    • 텍스트 노드의 내용이 변경될 때 이를 감지할 수 있습니다.
    • 주로 요소의 텍스트 내용이 동적으로 변경되는 경우에 유용합니다.
    const observer = new MutationObserver((mutationsList) => {
      mutationsList.forEach((mutation) => {
        if (mutation.type === 'characterData') {
          console.log('텍스트 내용이 변경되었습니다.');
          if (mutation.oldValue) {
            console.log(`이전 값: ${mutation.oldValue}`);
          }
        }
      });
    });
    
    observer.observe(targetElement, { characterData: true, characterDataOldValue: true });
    
  4. 하위 노드(subtree) 변경

    • subtree: true 옵션을 사용하면 대상 요소뿐만 아니라 모든 하위 노드의 변화를 감지할 수 있습니다.
    const observer = new MutationObserver((mutationsList) => {
      mutationsList.forEach((mutation) => {
        console.log('변화 감지:', mutation);
      });
    });
    
    observer.observe(targetElement, { childList: true, attributes: true, characterData: true, subtree: true });
    

ResizeObserver

ResizeObserver 인터페이스는 요소의 컨텐츠 또는 border box의 크기, 혹은 SVGElement의 bounding box에 대한 변경을 감지합니다. (by MDN)

ResizeObserver는 DOM 요소의 크기(너비와 높이) 변화를 감지하는 데 사용되는 웹 API입니다. 이는 반응형 디자인이나 동적으로 크기가 변경되는 요소를 추적할 때 유용하며, 요소의 크기가 변경될 때마다 콜백 함수를 호출해 해당 변화를 처리할 수 있게 해줍니다.

예제 코드

// 1. ResizeObserver 인스턴스 생성 및 콜백 함수 정의
const resizeObserver = new ResizeObserver((entries) => {
  entries.forEach((entry) => {
    console.log('변화가 감지된 요소:', entry.target);
    console.log('새로운 크기:', entry.contentRect.width, 'x', entry.contentRect.height);
  });
});

// 2. 관찰할 대상 요소 지정
const targetElement = document.getElementById('myElement');
resizeObserver.observe(targetElement);

// 3. 감지 중지 (필요한 경우)
// resizeObserver.unobserve(targetElement);

// 4. 모든 관찰 해제 (필요한 경우)
// resizeObserver.disconnect();

ResizeObserver 또한 비동기적으로 처리합니다

ResizeObserver에 등록되는 콜백 함수는 비동기적으로 실행되며, 이벤트 루프를 통해 처리됩니다. 이는 앞서 설명한 MutationObserver와 같은 이유로, 덕분에 성능을 최적화하고 레이아웃 리플로우로 인한 렌더링 지연을 방지할 수 있습니다.

Observation Errors

MDN에서는 Observation Errors라는 소제목의 섹션을 따로 두어 ResizeObserver를 사용할 때 발생할 수 있는 오류 상황에 대해 설명하고 있습니다.

  1. 무한 루프 발생 가능성

    ResizeObserver는 요소의 크기 변화를 감지하고, 그에 따라 콜백 함수를 실행합니다. 그러나 콜백 함수 내에서 요소의 크기를 변경하는 코드를 작성하면, 다시 그 변화가 ResizeObserver에 감지되어 또다시 콜백이 실행됩니다. 이 과정이 반복되면 무한 루프가 발생합니다.

    const resizeObserver = new ResizeObserver(entries => {
      for (const entry of entries) {
        // 콜백 내에서 요소의 크기를 변경함
        entry.target.style.width = entry.contentRect.width + 1 + 'px';
      }
    });
    
    resizeObserver.observe(document.querySelector('#myElement'));
    

    위 코드에서 콜백 함수는 요소의 너비를 계속 증가시키므로, ResizeObserver는 끊임없이 크기 변화를 감지하고 콜백을 호출하게 됩니다.

  2. 관찰 루프(limit) 초과

    브라우저는 이러한 무한 루프를 방지하기 위해 ResizeObserver의 관찰 루프에 제한을 둡니다. 만약 루프가 특정 횟수를 초과하면 "ResizeObserver loop limit exceeded"라는 오류가 발생합니다. 이는 브라우저가 무한 루프를 감지하고 스크립트의 실행을 중단시켰다는 의미입니다.

  3. 의도적인 무한 루프

    만약 의도적으로 ResizeObserver가 무한 루프에 빠지게 하려면, ResizeObserver 콜백에 등록한 resizing code가 실행되는 것을 브라우저의 리페인트 이후 시점으로 지연시키면 됩니다. 그러기 위해서는 requestAnimationFrame 콜백에 resizing code를 넣으면 되죠.

    const divElem = document.querySelector("body > div");
    
    const resizeObserver = new ResizeObserver((entries) => {
      requestAnimationFrame(() => {
        for (const entry of entries) {
          entry.target.style.width = entry.contentBoxSize[0].inlineSize + 10 + "px";
        }
      });
    });
    
    resizeObserver.observe(divElem);
    
    window.addEventListener("error", (e) => {
      console.error(e.message);
    });
    

IntersectionObserver

IntersectionObserver 인터페이스는 대상 요소와 상위 요소, 또는 대상 요소와 최상위 문서의 뷰포트가 서로 교차하는 영역이 달라지는 경우 이를 비동기적으로 감지할 수 있는 수단을 제공합니다. (by MDN)

예제 코드

const images = document.querySelectorAll('img[data-src]');

const options = {
  root: null,
  rootMargin: '0px',
  threshold: 0.1
};

const callback = (entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
};

const observer = new IntersectionObserver(callback, options);

images.forEach(img => {
  observer.observe(img);
});

역시나 IntersectionObserver도 핸들러를 콜백 함수로 받아 비동기 처리합니다

IntersectionObserver는 관찰 대상 요소의 가시성 변화를 비동기적으로 감지하고, 이에 따라 콜백 함수를 호출합니다. 이는 다음과 같은 이유로 설계되었습니다:

  • 메인 스레드 차단 방지: 비동기적으로 콜백을 호출함으로써 메인 스레드의 작업 흐름을 방해하지 않습니다.
  • 성능 최적화: 스크롤 이벤트처럼 빈번하게 발생하는 이벤트를 동기적으로 처리하면 성능 저하가 발생할 수 있습니다. 비동기 처리는 이러한 문제를 완화합니다.
  • 렌더링 일관성 유지: 브라우저의 렌더링 사이클과 조화를 이루어 시각적 일관성을 유지합니다.

참고 자료