C.W.K.
Stream
Lesson 03 of 06 · published

Isolated world — 같은 DOM, 다른 JavaScript

~12 min · isolated-world, security, content-script, scripting, world-main

Level 0Extension 입덕
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"같은 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 가 자기 HTMLElement constructor 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:

  1. DOM event. Page 가 알려진 element 에서 CustomEvent dispatch; content script 가 listen. Content script 가 dispatch; page 가 listen. 가장 isolated 하고 가장 explicit 한 패턴.
  2. window.postMessage. 두 world 가 같은 window instance (DOM-bound object) 공유해서 거기 message post 가능. data payload 는 world 간 structured-clone — primitive / plain object / array 는 생존; function 과 class instance 는 안 됨.
  3. 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.executeScriptworld: '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 유지.

같은 DOM, 다른 JavaScript. Isolation 이 default; MAIN world 는 explicit 한 좁은 escape hatch. ISOLATED 에서 DOM event 나 postMessage 로 풀 수 있으면 그렇게 — MAIN world 는 보안과 편의를 거래.
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 로 다시 보내.

Code

ISOLATED-world content script — DOM yes, page global no·javascript
// content.js (ISOLATED world — default)
// Page 가 정의한 게 우리한테 invisible, 반대도 마찬가지.

window.clipdeckMarker = "hello from ClipDeck content script";

// 가상의 page global read 시도. 거의 항상 undefined.
console.log("[ClipDeck content] window.gtag is:", window.gtag);

// DOM 공유 확인.
const h1 = document.querySelector("h1");
if (h1) console.log("[ClipDeck content] first h1 text:", h1.textContent);

// Prototype identity 가 world 별인 거 시연.
console.log("[ClipDeck content] HTMLElement is:", HTMLElement);
MAIN-world injection — SW 에서 page-defined global read·javascript
// background.js — MAIN world 에 inject 해서 page global read
chrome.action.onClicked.addListener(async (tab) => {
  if (!tab.id) return;
  const [result] = await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    world: "MAIN",
    func: () => {
      // 이제 page 자체 JS world 안에 있음.
      // window.gtag (있다면) 여기서 닿음.
      return {
        hasGtag: typeof window.gtag === "function",
        hasReact: typeof window.React !== "undefined",
        documentTitle: document.title,
      };
    },
  });
  console.log("[ClipDeck SW] page-world snapshot:", result.result);
});
ISOLATED ↔ MAIN bridge via window.postMessage with sentinel·javascript
// content.js (ISOLATED) — window.postMessage 로 MAIN-world helper 와 대화
window.addEventListener("message", (event) => {
  // 항상 origin 과 sentinel field 검증 후에 payload 신뢰.
  if (event.source !== window) return;
  if (event.data?.source !== "clipdeck-page-script") return;

  console.log("[ClipDeck content] received from page:", event.data.payload);
});

// Page-world 스크립트에 요청 보냄
window.postMessage(
  { source: "clipdeck-content-script", type: "giveMeReactVersion" },
  "*"
);

External links

Exercise

clipdeck/content.js 의 body 를 첫 번째 code block 으로 교체. window.gtag 정의된 page 아무거나 (대부분의 news site 와 대형 retailer 가 그래 — cnn.com 이나 amazon.com 시도) 열고, page DevTools 열고, Console context 를 'ClipDeck' 으로 전환, window.gtag is: undefined 줄 확인. 이제 Console context 를 'top' 으로 전환하고 window.gtag 타이핑 — function 이나 object. 같은 DOM, 다른 JS. 다음 두 번째 code block 을 clipdeck/background.js 에 추가 (Lesson 2 exercise 처럼 popup 잠깐 다시 제거 필요할 수도). Toolbar icon 클릭 — SW console 에 hasGtag: true 보고돼야 함. MAIN-world injection 이 page 의 실제 gtag 에 닿았고; ISOLATED-world content script 는 못 닿았어.
Hint
두 context 다 gtag 보이면 실수로 Wikipedia 등 gtag 없는 page 열린 거 — commerce 나 news site 시도. MAIN-world injection 이 world is not a valid option throw 하면 Chrome 이 95 보다 옛 거 (현대 install 에선 매우 드묾 — chrome://version 확인). 분명 analytics 있는 site 에서 SW console 이 hasGtag: false 보고하면, 그 site 가 클릭 시점에 아직 fire 안 한 방식으로 lazy-load 중 — navigation 후 몇 초 기다리고 다시 클릭.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.