"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
- Iframe 이 bridge 한테 context 요청 — iframe 이
window.parent.postMessage({type:'pippa:request-context', requestId}, '*'); bridge 의 message listener 가 잡고, SW 한테 묻고, 결과를 iframe 으로 다시 post. - SW 가 panel 에 context push — content script 가 새 context 보고할 때, SW 가
chrome.runtime.sendMessage통해 broadcast; bridge 의 chrome.runtime.onMessage listener 가 잡고 iframe 에 post. - Iframe boot — iframe load 와 bridge load 시 bridge 가 requestContext() 호출해서 SW 가 이미 가진 context 로 iframe seed.
- 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.