DOM access — Selection, listener, MutationObserver
~14 min · dom, selection, events, mutation-observer, spa
Level 0Extension 입덕
0 XP0/56 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Content script가 DOM을 가졌어. Lesson 4는 모든 feature에서 반복할 네 가지 동작: node 찾기, selection read, event listen, SPA reshuffle 살아남기."
Node 찾기
Content script는 DOM API 전체 가짐 — 어떤 web page 든 가진 그것. 끊임없이 손이 가는 네 가지:
document.querySelector(selector) — 첫 매칭, 또는 null.
document.querySelectorAll(selector) — 매칭 전체의 NodeList (snapshot, live 아냐).
document.getElementById(id) — 알려진 id의 가장 빠른 경로.
node.closest(selector) — 부모 매칭될 때까지 tree 위로. click target이 안쪽 span 인데 row가 필요한 event delegation에 필수.
CSS-selector syntax. 스타일이 쓰는 같은 거. jQuery 필요 없음. querySelector가 div.row[data-id="42"] > button:not(:disabled)를 직접 처리.
Selection read
Selection API가 ClipDeck Track 3 milestone이 올라타는 거. window.getSelection()이 document의 현재 highlight 텍스트 기술하는 Selection object 반환. 쓸 두 method:
selection.toString() — highlight 된 range의 plain text.
selection.getRangeAt(0) — anchor/focus node, start/end offset, highlight 근처 floating UI 위치 잡는 getBoundingClientRect() 가진 Range object.
아무것도 안 고른 상태 (커서만 깜박이는 경우) 면 toString()이 빈 문자열을 줘. 그러니 항상 걸러 줘. 선택은 user가 다른 데를 누르거나 스크립트가 지우기 전까지는 event를 넘어서도 남아 있어. 그래서 mouseup handler 안에서 읽으면 믿을 만한데, 500ms 뒤 setTimeout 안에서 읽으면 대개 이미 없어.
Event listening
ClipDeck 유용성 순으로 세 패턴:
document에 몰아서 걸기. listener 하나를 document.addEventListener('mouseup', handler)로 걸어 두고 event.target.closest(...)로 걸러 내. handler가 뿌리에 붙어 있으니까 갈려 나갈 노드에 매이지 않아서, DOM이 아무리 갈아엎여도 공짜로 살아남아.
내려갈 때 잡기.addEventListener(event, fn, { capture: true })로 걸면 페이지 자기 handler 보다 먼저 돌아. 페이지가 올라오는 단계에서 stopPropagation을 불러 버려서 우리 handler까지 event가 안 올 때 쓸모 있어. 대신 아껴 써. 페이지보다 먼저 돌면서 event를 건드리면 페이지 로직을 깨뜨릴 수 있거든.
Single-page app (Gmail, Twitter/X, YouTube, React 적인 거 전부)가 user navigate 할 때 DOM의 큰 부분을 tear down 하고 rebuild. Load 시점에 header에 inject 한 버튼은 framework가 re-render 하는 순간 사라져.
Fix는 MutationObserver. Reshuffle 일어나는 영역 구독, 관련 변화 도착할 때마다 injection logic 재실행. 두 가지 포인터:
지켜볼 범위는 최대한 좁게 잡아. document.body에 subtree: true를 걸어도 돌아가긴 하는데 쉴 새 없이 울려. 특정 컨테이너 하나만 보는 게 훨씬 싸.
이미 처리한 node 알기 위해 marker로 data-* attribute 사용. if (node.dataset.clipdeckHandled) return; node.dataset.clipdeckHandled = '1'; — idempotent injection.
Idempotency 습관
Inject 하는 것 — 버튼, 스타일시트, event listener — 무엇이든 스크립트가 두 번 돌 거라 가정: 첫 load 한 번, MutationObserver tick 후 한 번, 가끔 tab restore 후 세 번째. 모든 insertion을 반복해도 안전하게:
Node 삽입 전 id 나 marker attribute로 확인.
이미 붙인 listener를 기억해 뒀다가 같은 노드에 또 붙이지 마.
스타일은 <style id="clipdeck-css"> 태그 하나 쓰고 새 태그 append 보다 그 textContent update.
우리가 들어가는 페이지는 우리 것이 아니야. 우리가 심어 둔 것들을 곱게 대해 줄 이유도 없고. 몇 번을 돌려도 같은 결과가 나오게 짜 두는 게 최소한의 자기방어야.
Node 찾고, selection read 하고, document에서 listen 하고, reshuffle 살아남기. 대부분의 content-script 버그가 이 네 가지 중 하나 위반에서 옴 — 보통 마지막.
iframe 안 selection.window.getSelection()은 이 document의 selection만 봐. User가 YouTube 댓글 (same-origin iframe 안에 사는) 이나 cross-origin embed 안 텍스트 highlight 하면, top-level content script는 아무것도 read 안 함. iframe URL을 matches에 추가하고 manifest에서 all_frames: true 설정하든가, nested-frame selection은 scope 밖이라고 받아들이든가. ClipDeck v1은 안 쫓아. v2는 그럴 수도.
Code
mouseup 시 selection capture — 텍스트 + bounding rect read·javascript
// content.js — selection capture skeleton
function readSelection() {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return null;
const text = selection.toString();
if (!text.trim()) return null;
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
return { text, rect: { top: rect.top, left: rect.left, width: rect.width, height: rect.height } };
}
document.addEventListener("mouseup", () => {
const result = readSelection();
if (!result) return;
console.log("[ClipDeck content] selection:", result.text);
console.log("[ClipDeck content] at:", result.rect);
// Track 3 lesson 6 가 이 log 를 save action 으로 바꿈.
});
clipdeck/content.js를 두 번째 code block (idempotent 버튼 injection)으로 교체. Extension reload. 세 page — wikipedia article, github repository view, youtube video — 열기. 각각의 우상단 floating 📎 Save to ClipDeck 버튼 찾기. youtube에서 home page에서 video로, 다시 home으로 navigate. 버튼이 SPA transition에서 사라져? MutationObserver 덕에 한 tick 안에 다시 나타나야 함. 이제 wikipedia article에서 텍스트 selection 하고 버튼 클릭 — SW DevTools console 열어 saveClip 메시지 도착 확인 (SW handler는 Lesson 6에서 wire).
Hint
버튼이 안 나타나면 manifest의 content_scripts.matches에 <all_urls> 아직 있고 content.js 편집 후 extension reload 했는지 확인. 버튼이 나타났는데 SPA transition에서 영구히 사라지면 MutationObserver가 fire 안 하는 것 — 파일 맨 아래에 observer.observe(document.body, { childList: true, subtree: true }) 있는지 확인. 버튼이 page 자체 UI와 시각적으로 충돌하면 z-index 더 높이거나 우하단으로 옮겨. ClipDeck의 최종 design은 매 page의 fixed 버튼 아닌 작은 unobtrusive corner badge 사용.
Progress
Progress is local-only — sign in to sync across devices.