본문 바로가기
C.W.K.
Stream
Lesson 04 of 10 · published

Anchor 4 — Popup가 doorway

~8 min · popup, side-panel, case-study, single-responsibility, v0.2.1

Level 0Extension 입덕
0 XP0/56 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"ChromeEmbed v0.1의 popup.js가 여섯 줄이었어. Lesson 4가 그게 맞는 숫자였던 이유 — 그리고 'just one more popup feature' 손 뻗을 때 일어나는 것."

여섯 줄

document.getElementById('open-panel')?.addEventListener('click', async () => {
  const windowInfo = await chrome.windows.getCurrent().catch(() => null);
  if (windowInfo?.id !== undefined) {
    await chrome.sidePanel.open({ windowId: windowInfo.id }).catch(() => {});
  }
  window.close();
});

Handler 하나. 'Open Panel' 버튼 click → 현재 window fetch → 그 window에서 side panel 열기 → popup 닫기. Popup HTML이 비슷하게 tiny — 'Open Panel' label 된 styled 버튼.

이게 맞는 이유

  • Single responsibility — popup이 launcher. 그 이상은 panel과 실제 경험 surface 로서 경쟁.
  • 한 살이가 뻔해 — popup이 열리고, popup이 닫혀. 관리할 상태도, 다시 그릴 일도, 쫓아다닐 경합도 없어.
  • 빠른 paint — popup HTML load, JS 돔, click handler attach. 아마 open 부터 ready 까지 50 ms 미만.
  • 경쟁 UI 없음 — Pippa가 panel iframe 에 살아. popup이 parallel chat UI host 안 하고 user를 그것 가리키는 게 mental model singular 유지.

안 동작하는 대안

Popup useful 하게 만들고 싶으면? Quick-chat input, most-recent-message preview, brain selector 추가. 각각:

  • Panel 에 이미 존재하는 UI 중복.
  • panel iframe이 이미 하는 cwkPippa backend 에 메시지 필요 — popup 에 그 messaging 재구현하거나 SW → iframe (오늘 깔끔한 API 없음) 통해 route.
  • 한 살이 자체가 문제야. user가 popup 바깥을 건드리는 순간 popup이 죽어. 그때 돌고 있던 일도 같이 날아가고.

'popup 도 쓸모 있게 만들자' 는 설계는 예외 없이 중복이라는 값을 치러. 문으로만 쓰는 popup은 그 값을 아예 안 내고. ChromeEmbed는 안 내는 쪽을 골랐어.

Window vs Tab 선택

Popup이 chrome.sidePanel.open 에 { windowId } 전달. 두 결과:

  • panel은 창 하나 단위로 열려. 그 창 안에서 tab을 옮겨 다녀도 panel은 그대로 있고, 내용만 지금 보고 있는 tab 에 맞춰 바뀌어. background 버스랑 Lesson 5의 다리를 타고서.
  • 닫는 건 Chrome이 원래 달아 둔 side-panel 조작부로 하면 돼.

{ tabId } 대신 전달하면 그 한 tab 으로 panel scope. user가 tab 전환하는 순간 panel 닫힘. 전체 window의 browsing session 너머 ambient 되고 싶은 household extension 엔 잘못된 UX.

Optional chaining

document.getElementById('open-panel')?.addEventListener(...) — optional chaining이 paranoia. Popup HTML이 바뀌고 버튼 id mis-spell 되면, popup이 gracefully degrade (JS error 없음, 그냥 non-functional 버튼). 6-줄 script 엔 overkill. 백 git revision 후 update 할 수도 있는 extension 엔 courtesy. 유지.

Popup이 doorway, panel이 destination. 여섯 줄이 충분. 그 이상은 같은 workflow 공간 위해 panel과 경쟁. Popup feature 안 추가 비용이 정확히 0. 추가 비용이 영구 duplication.
Popup status info가 필요하다면... 맞는 move가 상호작용 안 부르는 tiny preview (한 줄: 'Currently capturing context from this tab') 를 popup 에 렌더. chrome.storage에서 read 하거나 SW 에 메시지. open 시 한 번 렌더. 실제로 뭔가 하려면 user가 'Open Panel' click. Input field 추가 안 함, chat 추가 안 함 — popup을 status-and-launcher 만으로 유지.
ChromeEmbed v0.2 툴바 popup — Side Panel, Dock Mode, Overlay Mode 세 모드 버튼과 그 아래 Auto로 맞춰진 Frontend 선택기, 그리고 모든 모드가 같은 피파 sidekick 패널을 쓴다는 한 줄.
v0.2 popup — 여전히 doorway 인데, 문이 셋이고 셋 다 같은 방으로 이어진다는 안내가 붙었어.

Code

popup.html — entire file: 버튼 하나, scoped style·html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Pippa</title>
    <style>
      body { width: 240px; padding: 12px; margin: 0; font-family: system-ui, sans-serif; }
      button { width: 100%; padding: 10px 14px; font: inherit; cursor: pointer; }
    </style>
  </head>
  <body>
    <button id="open-panel">Open Panel</button>
    <script src="popup.js"></script>
  </body>
</html>
popup.js — entire file: 여섯 줄, 한 job·javascript
document.getElementById('open-panel')?.addEventListener('click', async () => {
  const windowInfo = await chrome.windows.getCurrent().catch(() => null);
  if (windowInfo?.id !== undefined) {
    await chrome.sidePanel.open({ windowId: windowInfo.id }).catch(() => {});
  }
  window.close();
});
v0.2.1 checkpoint — popup.html — 문 셋, shared Sidekick 하나·html
<button id="open-sidepanel">Side Panel</button>
<button id="open-dock">Dock Mode</button>
<button id="open-overlay">Overlay Mode</button>
<label for="panel-origin">Frontend</label>
<select id="panel-origin">
  <option value="auto">Auto</option>
</select>
<p>All modes use the same Pippa sidekick panel.</p>
v0.2.1 checkpoint — popup.js — native side panel과 content-script mode 갈림길·javascript
async function setMode(mode) {
  const windowInfo = await chrome.windows.getCurrent().catch(() => null);
  if (mode === "sidepanel") {
    await chrome.storage.local.set({ pippaEmbedDisplayMode: "sidepanel" });
    await chrome.sidePanel.setOptions({
      path: "sidepanel.html?mode=sidepanel",
      enabled: true,
    });
    await chrome.sidePanel.open({ windowId: windowInfo.id });
  } else {
    await chrome.runtime.sendMessage({
      type: "pippa:set-display-mode",
      mode,
      windowId: windowInfo?.id,
    });
  }
  window.close();
}

External links

Exercise

실제 popup.html과 popup.js를 열고 Side Panel button과 Dock button을 따로 따라가. Direct user gesture가 어디서 소비되는지, display preference가 어디에 남는지, content script는 어디서 호출되는지, popup.html에 왜 Auto origin literal만 있는지 표시해.
Hint
제안한 popup feature가 conversation state나 message history를 필요로 하면 /embed/panel 소유야. Mode와 endpoint 선택이 doorway 책임이야.

Progress

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

댓글 0

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

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