C.W.K.
Stream
Lesson 05 of 08 · published

Anchor 5 — Side panel 이 iframe bridge

~12 min · side-panel, iframe, postMessage, bridge

Level 0Extension 입덕
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"ChromeEmbed 의 side panel 이 cwkPippa 가리키는 iframe 하나 가진 HTML page. Lesson 5 가 chrome.* 없는 iframe 이 extension 과 iframe 사이 window.postMessage 통해서도 SW 가 모으는 context 얻게 해 주는 bridge."

Side panel HTML

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Pippa</title>
    <style>
      html, body { margin: 0; width: 100%; height: 100%; background: #0f0f23; }
      iframe { width: 100%; height: 100vh; border: 0; display: block; }
    </style>
  </head>
  <body>
    <iframe id="pippa-frame" src="http://localhost:5173/embed/panel"></iframe>
    <script src="sidepanel.js"></script>
  </body>
</html>

HTML 이 거의 전적으로 chrome (full-height iframe, load 중 dark background). src 가 cwkPippa 의 /embed/panel route 가리킴. manifest 의 extension_pages CSP 의 frame-src 가 localhost URL load 허용.

Identity 경계

Iframe 이 extension page 와 다른 origin (extension page 가 chrome-extension://<id>/ 에, iframe 이 http://localhost:5173/ 에). 결과:

  • Iframe 이 chrome.* 직접 read 못 함. 자기 perspective 에서 일반 web page.
  • Extension page (sidepanel.html + sidepanel.js) 가 chrome.* read CAN — extension 나머지와 같은 origin.
  • 둘이 공유 DOM window 의 window.postMessage 통해 communicate.

이게 ChromeEmbed 가 만드는 거래: iframe 으로 load 해서 cwkPippa 의 UI 모두 무료로 얻기, 어떤 chrome.* data 든 bridge 필요한 비용 지불.

Bridge — sidepanel.js

총 40 줄. Full surface:

const frame = document.getElementById('pippa-frame');

function postToPanel(message) {
  frame?.contentWindow?.postMessage(message, '*');
}

async function requestContext(requestId) {
  const response = await chrome.runtime
    .sendMessage({ type: 'pippa:request-context', requestId })
    .catch(() => null);
  if (response?.type === 'pippa:host-context') {
    postToPanel({ ...response, requestId: response.requestId || requestId });
  } else if (requestId) {
    postToPanel({ type: 'pippa:no-context', requestId });
  }
}

chrome.runtime.onMessage.addListener((message) => {
  if (message?.type === 'pippa:host-context') {
    postToPanel(message);
  }
});

window.addEventListener('message', (event) => {
  if (event.data?.type === 'pippa:request-context') {
    requestContext(event.data.requestId);
  }
});

frame?.addEventListener('load', () => requestContext());
requestContext();

네 flow

  1. Iframe 이 bridge 한테 context 요청 — iframe 이 window.parent.postMessage({type:'pippa:request-context', requestId}, '*'); bridge 의 message listener 가 잡고, SW 한테 묻고, 결과를 iframe 으로 다시 post.
  2. SW 가 panel 에 context push — content script 가 새 context 보고할 때, SW 가 chrome.runtime.sendMessage 통해 broadcast; bridge 의 chrome.runtime.onMessage listener 가 잡고 iframe 에 post.
  3. Iframe boot — iframe load 와 bridge load 시 bridge 가 requestContext() 호출해서 SW 가 이미 가진 context 로 iframe seed.
  4. Future bidirectional — iframe 이 'SW 에 이거 보내' 보내면, bridge 의 message listener 가 chrome.runtime.sendMessage 로 route 가능. v0.1 엔 아직 그 경로 없음.

Loose origin

postMessage 호출이 target origin 으로 '*' 사용. Public page 에선 보안 risk — 어떤 iframe 이든 payload 받을 수 있음. Loading 제어하는 panel 안에서는 받아들일 만: 유일한 iframe 이 spawn 한 것. Production-hardening 이 '*' 를 실제 cwkPippa origin 으로 바꿈 (iframe 도 부모로 돌아가는 메시지에 같이 함).

Iframe 쪽 모습

cwkPippa 의 /embed/panel route 안, 코드가 대략 bridge mirror:

// cwkPippa 안, embed/panel React component
useEffect(() => {
  const requestId = Math.random().toString(36).slice(2);
  function onMessage(event) {
    if (event.data?.requestId === requestId && event.data?.type === 'pippa:host-context') {
      setContext(event.data.payload);
    }
  }
  window.addEventListener('message', onMessage);
  window.parent.postMessage({ type: 'pippa:request-context', requestId }, '*');
  return () => window.removeEventListener('message', onMessage);
}, []);

Track 7 Lesson 6 의 preview-and-confirm Promise 패턴이 이걸 nicely generalize 가능; ChromeEmbed v0.1 이 inline 유지.

왜 native render 대신 iframe

대안은: dist/ 에 사는 React build 사용해서 cwkPippa 의 panel chat 을 extension package 안에 재구현. 동작하지만 매 cwkPippa 변경이 extension rebuild 도 필요. Iframe 경로가 cwkPippa 를 source of truth, extension 이 순전히 delivery mechanism.

ChromeEmbed v0.1 엔 iframe 이 멀찌감치 이김 — cwkPippa 가 이미 몇 달 UI 투자 가진 working SPA. 'extension-shape cwkPippa' build 가 surface area 두 배. v2 가 invert 가능 — embed API 가 안정화되면, offline-capable bundled UI 가 중요할 수도 — 그건 일부러 defer.

Side panel = 실제 app 으로의 iframe 가진 HTML. chrome.* 와 iframe 사이 bridge 가 window.postMessage. 세 짧은 flow: iframe ask / SW push / iframe boot. 40 줄 bridge 가 다 처리.
Dev-vs-prod URL 선택. sidepanel.html 이 localhost:5173 hardcode. Prod 엔 cwkPippa 의 배포 URL 로 swap. 둘 다 frame-src 에 있어야; 둘 다 panel 렌더할 때 reachable 해야. v0.1 이 dev 편의 위해 localhost 와 ship; Track 8 build step 이 NODE_ENV-style flag 기반 swap 가능.

Code

sidepanel.js — entire file: chrome.runtime ↔ iframe postMessage bridge·javascript
// embeds/chrome/sidepanel.js — full 40-줄 bridge
const frame = document.getElementById('pippa-frame');

function postToPanel(message) {
  frame?.contentWindow?.postMessage(message, '*');
}

async function requestContext(requestId) {
  const response = await chrome.runtime
    .sendMessage({ type: 'pippa:request-context', requestId })
    .catch(() => null);
  if (response?.type === 'pippa:host-context') {
    postToPanel({ ...response, requestId: response.requestId || requestId });
  } else if (requestId) {
    postToPanel({ type: 'pippa:no-context', requestId });
  }
}

chrome.runtime.onMessage.addListener((message) => {
  if (message?.type === 'pippa:host-context') {
    postToPanel(message);
  }
});

window.addEventListener('message', (event) => {
  if (event.data?.type === 'pippa:request-context') {
    requestContext(event.data.requestId);
  }
});

frame?.addEventListener('load', () => requestContext());
requestContext();

External links

Exercise

embeds/chrome/sidepanel.html 과 sidepanel.js end to end read. Panel DevTools console 에서 frame.contentWindow.postMessage({type:'pippa:request-context', requestId:'test123'}, '*') 타이핑하고 bridge 가 request 를 SW 로 route, SW 가 context return, bridge 가 iframe 으로 응답 post 하는 거 watch. Roundtrip 이 entire bridge surface — 세 짧은 hop, 하나의 promise resolve. 다음 panel DevTools 에서 chrome.runtime.sendMessage({type:'pippa:request-context', requestId:'test456'}) 시도 — extension 과 same-origin 이라 SW 응답 직접 얻음. Iframe 이 그 luxury 없음, bridge 가 존재하는 이유.
Hint
DevTools 의 postMessage 가 아무것도 trigger 안 하면, panel iframe id (pippa-frame) 와 panel DevTools (iframe 아님) 에서 타이핑하는지 더블 체크. Console context dropdown 으로 extension page 와 iframe 간 전환 — 각자 다른 chrome.* availability 봄. Lesson: 모든 layer 가 다른 권한; 그것들 bridge 가 extension 이 먹고 사는 것.

Progress

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

댓글 0

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

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