DOM access — Selection, listener, MutationObserver
~14 min · dom, selection, events, mutation-observer, spa
Level 0Extension 입덕
0 XP0/54 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.
길이 0 selection (cursor blink 만) 은 toString() 이 빈 문자열 반환. 항상 guard. Selection 은 user 가 다른 데 클릭하거나 스크립트가 clear 할 때까지 DOM event 너머로 유지 — mouseup handler 안에서 read 하면 reliable, 500ms 뒤 setTimeout 안에서 read 하면 보통 안 됨.
Event listening
ClipDeck 유용성 순으로 세 패턴:
document 위 delegation. 한 listener 를 document.addEventListener('mouseup', handler) 로 등록하고 event.target.closest(...) 로 filter. Handler 가 root 에 있어서 swappable node 가 아니라, DOM reshuffle 을 공짜로 살아남음.
Capture phase.addEventListener(event, fn, { capture: true }) 가 page 자체 handler 전에 핸들러 실행. Page 가 bubble phase 에서 stopPropagation 부르고 handler 가 event 못 받을 때 유용. 아껴 써 — page 전에 도는 게 event mutate 하면 page logic 깰 수 있음.
Passive listener.scroll / touchmove 에 { passive: true } — 실수로 preventDefault 못 부르게. Chrome 한테 cancellation check 건너뛰고 스크롤 부드럽게 유지하라고 알려 줘.
MutationObserver — SPA reshuffle 살아남기
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 재실행. 두 가지 포인터:
가능한 좁은 subtree 관찰. document.body 에 subtree: true 도 동작하지만 끊임없이 fire; 특정 container 관찰이 훨씬 저렴.
이미 처리한 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 로 확인.
Attach 한 listener 추적해서 같은 node 에 재attach 안 함.
스타일은 <style id="clipdeck-css"> 태그 하나 쓰고 새 태그 append 보다 그 textContent update.
방문하는 page 는 우리 거 아냐. Inject 된 상태에 친절할 리 없음. Idempotency 가 최소 자기방어.
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.