"같은 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는 양쪽에서 다 true로 나오는데, 사실 각 세계가 자기만의HTMLElement를 쥐고 있는 거야.
왜 존재해
보안과 안정성, 양방향:
- Extension 으로부터 page 보호.
window.fetch를 덮어쓴 buggy extension이 page 깨뜨릴 텐데, isolation이 그걸 막아. - 페이지로부터 extension을 지켜. 못된 페이지가 자기 세계에
function chrome() { … }를 만들어 둬도, extension의chrome.runtime.sendMessage호출을 거기로 빼돌릴 수 없어. 격리가 그걸 막아 주거든. - extension 끼리도 안 부딪혀. 두 extension이 같은 페이지에 들어가도 각자 자기 세계를 받아. 서로의 전역이 충돌할 일이 없어.
이 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. 두 세계가 같은window하나를 같이 봐 (이건 DOM에 매인 물건이거든). 그래서 거기에 메시지를 던질 수 있어. 넘어가는data는 복제돼서 건너가. 숫자나 문자열, 평범한 object와 배열은 무사히 넘어가고, 함수나 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에서 페이지와 다리를 놓는 방법을 돌아가는 코드로 다룰 거야. 다만 ClipDeck이 필요로 하는 일 대부분 — 선택 영역 읽기, 페이지 정보 읽기, clip 저장 — 에는 다리가 필요 없어. content script 자기 세계 안에 이미 다 있거든.
world: 'MAIN' escape hatch
Chrome 95+ 가 chrome.scripting.executeScript에 world: 'MAIN' 추가. Chrome 111+ 는 declarative manifest entry 에도 같은 option 확장. 이걸 주면 스크립트가 페이지 자기 JavaScript 세계에서 돌아. window.gtag 든 window.React 든 페이지가 만든 건 뭐든 다 만질 수 있게 되지. 대신 치를 값이 커. 격리를 통째로 잃고, 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로 다시 보내.world: "MAIN"으로 넣은 코드는 page code로 돌아. chrome.runtime, storage, messaging은 isolated partner에 두고 좁게 검증한 payload만 bridge해.