"같은 DOM, 다른 JavaScript. 이 한 문장만 받아들이면 다른 모든 content-script 퍼즐이 알아서 풀려. Lesson 3 는 뭐가 공유되고, 뭐가 안 공유되고, 정말 page 의 world 가 필요할 때 Chrome 이 주는 escape hatch 의 깊은 dive."
Two-world 모델
Chrome 이 tab 에 content script inject 할 때, 그것 위한 새 JavaScript context 만들어 — 분리된 heap, 분리된 global object, 분리된 class prototype set. 스크립트의 window 와 page 의 window 는 같은 underlying browser primitive 를 감싸지만 서로 다른 object.
실용적으로 의미하는 것:
- DOM 은 공유. 두 world 다 같은
document, 같은 element, 같은 attribute 봄. - JavaScript identity 는 안 공유. Page 의
window.fetch가 monkey-patch 된 버전일 수 있고; content script 의window.fetch는 깨끗한 browser 거. - Custom global 안 건너감. Page 가
window.gtag = ...하면, content script 는undefined봄. Content script 가window.clipdeckHook = ...하면, page 는 아무것도 안 봄. - Class prototype 도 독립.
document.querySelector('div') instanceof HTMLElement가 두 world 다 true 지만, 각 world 가 자기HTMLElementconstructor reference 가짐.
왜 존재해
보안과 안정성, 양방향:
- Extension 으로부터 page 보호.
window.fetch를 덮어쓴 buggy extension 이 page 깨뜨릴 텐데, isolation 이 그걸 막아. - Page 로부터 extension 보호. 자기 world 에
function chrome() { … }정의한 악성 page 가 extension 의chrome.runtime.sendMessage호출 redirect 못 함. Isolation 이 그걸 막아. - Multi-extension 안전성. 두 extension 이 같은 page 에 inject 하면, 각자 자기 world 받음. 두 global 이 충돌 안 함.
이 isolation 이 content script 를 쓸 만한 security primitive 로 만드는 핵심 — 그게 없으면 extension 과 page 는 끊임없는 군비 경쟁.
두 world 가 대화하는 법
선호 순서로 세 가지 정당한 bridge:
- DOM event. Page 가 알려진 element 에서
CustomEventdispatch; content script 가 listen. Content script 가 dispatch; page 가 listen. 가장 isolated 하고 가장 explicit 한 패턴. window.postMessage. 두 world 가 같은windowinstance (DOM-bound object) 공유해서 거기 message post 가능.datapayload 는 world 간 structured-clone — primitive / plain object / array 는 생존; function 과 class instance 는 안 됨.- Script tag inject. Pre-Chrome-111 경로: extension 안에 파일 작성,
web_accessible_resources에 list, content script 에서<script src="chrome-extension://.../injected.js">생성. Inject 된 파일이 page world 에서 돔. DOM event 나 postMessage 와 결합해 결과 받기.
Lesson 5 가 page bridge 를 working detail 로 다뤄. 대부분의 ClipDeck 필요 (selection read, page metadata read, clip 저장) 엔 bridge 필요 없음 — content script 자체 world 에 필요한 거 다 있음.
world: 'MAIN' escape hatch
Chrome 95+ 가 chrome.scripting.executeScript 에 world: 'MAIN' 추가; Chrome 111+ 는 declarative manifest entry 에도 같은 option 확장. 그걸로 스크립트가 page 자체 JavaScript world 에서 돔 — window.gtag, window.React, page 가 정의한 뭐든 full access. 대가: isolation 완전히 잃음, chrome.* API 잃음 (chrome.runtime 만 남음), page 가 스크립트 global 보고 수정 가능.
진짜로 page world 필요할 때 — page-side library 통합, framework 함수 monkey-patch, custom window.__store read — MAIN 사용. 다른 모든 건 ISOLATED 유지.
chrome ambiguity 함정. ISOLATED-world content script 에서 chrome 은 extension API. MAIN-world content script 에서 chrome 은 page 의 window.chrome — 훨씬 작은 browser-defined object (주로 chrome.webstore, installed app 의 chrome.runtime.id). MAIN script 가 chrome.runtime.sendMessage 부르면, 그 호출은 silent fail 하거나 misleading error throw. Extension API 호출은 ISOLATED partner script 에 두든가, 결과를 postMessage / DOM event 로 다시 보내.