이 글은 Chrome 확장 프로그램인 Neatified를 개발할 당시에 했던 고민을 엮어놓은 하나의 개발기입니다.


얼마 전에 제가 Neatified라는 하나의 작은 확장 프로그램을 발표했었습니다. 기능은 간단합니다. 이 확장 프로그램은 일부 사이트에서 사용하는 복사 방지와 같은 기능을 무시하고 웹 사이트 내용을 복사하게 해줍니다. 물론 그것이 저작권 표시로써 좋은 역할을 할 수 있을지라도 실제로는 전혀 그렇지 않다는 점에서 개발을 하게 되었습니다. 이번 경우에는 딱히 그렇게 큰 동기부여가 아닌 그냥 ‘뭐할까’ 고민하다가 아이디어가 튀어나온 케이스였습니다.

복사를 방해하는 요소 찾기

먼저 기능을 만들기 전에 생각해야할 요소들이 몇 가지 있습니다. 가장 직접적으로는 DOM 객체에 붙은 copy 이벤트입니다. copy 이벤트를 핸들링하면 복사하기 전에 [출처]...와 같은 문자열을 덧붙여서 다시 클립보드로 보낼 수 있습니다. 하지만 저희가 더 중점적으로 보아야 할 것은 그만큼 깔끔하게 처리해야 한다는 점입니다. copy 이벤트만이 복사를 방해하는 것은 아니었습니다.

또 하나 우리는 드래그를 방지하는 이벤트도 처리해야 했습니다. 그렇게 고민하고 직접 개발하고 사용하면서 몇 개의 이벤트를 발견했습니다.

  • copy
  • dragstart
  • contextmenu
  • selectstart

모든 사람이 Ctrl 키를 주로 사용하는 것은 아니었습니다. 그래서 우클릭을 방지하는 요소도 제거했습니다. 그리고 이후에 우클릭을 방지하는 또다른 요소를 발견했습니다. 이번에는 속성입니다.

  • oncontextmenu

그렇게 현재 Neatified에서는 총 5개의 요소를 제거하고 있습니다.

이벤트 핸들러 중지와 속성 제거

이제 남은 작업은 간단했습니다. 문제가 되는 이벤트와 속성을 중지하고 제거하면 끝입니다. 이벤트의 경우에는 아래와 같이 DOM 객체에 새로운 이벤트를 추가하고 확산을 중지시키면 핸들러가 작동을 멈춥니다. 이 때 addEventListener 메소드의 마지막 인수로 쓰인 true는 Capturing 방식을 사용할 것을 명시합니다.

element.addEventListener('copy', function (event) {
  event.stopPropagation()
}, true)

속성은 절대 jQuery가 아닌 순수 JavaScript로 removeAttribute 메소드를 사용하면 쉽게 제거할 수 있습니다.

element.removeAttribute('oncontextmenu')

하지만 removeAttribute 메소드와 같은 경우에는 DOM 객체 노드에만 적용할 수 있습니다. 문자열 노드와 같은 경우에는 해당 메소드를 사용할 수 없어 오류를 출력합니다. 현재 시점에서도 아직 DOM에서 모든 속성을 확인하지 않아 추후 지속적인 패치가 제공될 예정입니다. 마지막으로 모든 객체를 가져와 하나하나 이벤트와 속성을 제거합니다.

const elements = Array.from(document.querySelector('*'))

Array의 from 메소드를 활용하면 DOM 노드들을 배열로 쉽게 바꿔 forEach와 같은 메소드를 사용가능하게 해줍니다.

가상 DOM과 동적으로 생성되는 컨텐츠

하지만 모든 웹 사이트에서 확장 프로그램이 작동하지는 않았습니다. 그 이유는 간단합니다. 제가 CSR을 무시했기 때문이죠. 새로 생성된 DOM 객체는 DOMContentLoaded 이벤트 이후에 만들어져 실제로는 영향을 받지 않았습니다. 특히 React와 같은 프론트엔드 프레임워크를 사용하는 사이트에서요. 그래서 Google에서 열심히 찾아봤습니다. 그랬더니 JavaScript에서 예전에는 만질 일이 없었던 MutationObserver라는 아이를 찾아냈습니다. 이 MutationObserver라는 아이는 타겟 객체의 하위 객체에 변경 사항을 찾아내줍니다.

비록 콜백 함수를 사용해야 했지만 일단은 다른 방법(예를 들면 지속적으로 innerHTML의 length를 확인할 수 있음)보다는 훨씬 효율적으로 들렸습니다.

const observer = new MutationObserver(function (mutations, observer) {
  const nodeAdded = []

  mutations.forEach(function (mutation) {
    nodeAdded.push(...mutation.addedNodes)
  })

  neatified(...) // NOTE: 확장 프로그램 기능을 실행합니다
}
observer.observe(document.querySelector('html', { childList: true, subtree: tree })

MDN 문서에는 서술되어 있지는 않았지만 한 Stack Overflow의 소스에서 addedNodes 프로퍼티를 찾아냈고 잘 사용 중입니다.

이렇게 또 하나의 프로젝트 개발기를 마칩니다. 감사합니다.