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

Side panel 이 뭔지 — Chrome 의 영구 dock

~10 min · side-panel, concepts, ux, chrome-114

Level 0Extension 입덕
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Popup 은 tooltip. Side panel 은 dock. Track 4 는 ClipDeck 을 user 가 page 읽으면서 열어 둘 수 있는 surface 로 졸업시키기 — 책상 위 sticky note 와 notebook 의 차이."

Surface, 솔직하게

Side panel 은 Chrome 이 user tab 의 오른쪽 docked 영역에 렌더하는 extension-owned HTML document. 시각적으론 bookmark bar 와 toolbar 오른쪽 가장자리 사이. 기능적으론 popup 과 같은 extension page — 같은 chrome.* API access, 같은 isolated context, 같은 DevTools-inspectable.

핵심 차이는 lifetime: popup 은 user 가 다른 곳 click 하는 즉시 죽음; side panel 은 user 가 toolbar toggle 통해 명시적으로 닫을 때까지 열려 있음. ClipDeck 엔, user 가 article 읽으면서 clip list visible 하게 유지하고 저장 사이 아무것도 다시 열지 안 하고 새 clip 연달아 추가 가능.

History

chrome.sidePanel 이 stable Chrome 114, 2023 중반쯤 도착. 그 전엔 docked sidebar 흉내 유일한 방법은 content script 통해 iframe inject — strict CSP 가진 site 에서 fragile, cross-origin embed 에서 broken, view-source page 에서 doomed. Stable API 가 page DOM 과 전혀 충돌 안 하는 Chrome-owned 영역 줘.

오늘 만드는 거면 minimum 으로 Chrome 114 가정. Firefox 는 다른 shape 의 자체 sidebar API; 이 lesson 은 Chrome-specific. Track 8 의 packaging note 가 끝에서 cross-browser 전략 살짝 다룸.

세 가지 user-facing behavior

  • Toolbar-icon click 으로 열림 (chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }) 로 구성). 발견 가장 쉬움.
  • API 호출로 열림 — user-gesture handler 에서: context-menu entry, keyboard command, 또는 chrome.sidePanel.open() trigger 하는 popup 의 버튼. Custom shortcut bind 가능.
  • Tab switch 너머로 열려 있음 default 로. 표시된 panel content 는 tab 별로 바뀔 수 있지만 (다음 lesson 이 per-tab vs global 다룸), Chrome 이 user tab 전환 시 dock 자동 닫지 안 함.

안에 뭐가 살아

Side panel 은 그냥 panel.html 과 그 script / style. 원하는 거 뭐든 렌더 — vanilla DOM, React/Svelte/Solid app, self-hosted URL 가리키는 iframe. HTML 이 extension content security policy 아래 load 돼서, external script 와 inline event handler 는 CSP 넓히지 안 하면 차단 (Lesson 5 가 다룸).

Storage access 는 어디서나 같은 방식: mount 시 chrome.storage.local, live update 위해 chrome.storage.onChanged. SW 로 message passing 은 써 오던 같은 chrome.runtime.sendMessage.

왜 ClipDeck 이 panel 필요

Popup 이 첫 두 track 잘 봉사 — 작고, 빠르고, obvious. Clip 10 개 넘어가면 popup 답답. 50 개 넘어가면 unusable. Side panel 은 scale: timestamp / source URL / search / (Track 7) inline edit/delete affordance 가진 full-height list. Popup 은 quick-status surface ("오늘 clip 몇 개?") 와 "Open side panel" 버튼 hosting 자리로 계속 존재.

Popup = tooltip (transient, 작음, click 시 열림). Side panel = dock (persistent, full-height, dismiss 까지 유지). 다른 UX 니즈 cover; ClipDeck 은 둘 다 ship.
Chrome side panel vs Firefox sidebar. Chrome 의 chrome.sidePanel 과 Firefox 의 browser.sidebarAction 이 비슷해 보이지만 다른 lifecycle 모델과 살짝 다른 API. Cross-browser extension 은 보통 feature-detect 하고 두 구현 ship. 이 quest 는 Chrome 에 머묾; ClipDeck v2 가 Firefox parity 추가할 수도, 별개 관심사.

Code

manifest.json — version 0.6.0, side_panel.default_path 선언·json
{
  "manifest_version": 3,
  "name": "ClipDeck",
  "version": "0.6.0",
  "action": { "default_popup": "popup.html" },
  "background": { "service_worker": "background.js" },
  "side_panel": { "default_path": "panel.html" },
  "permissions": ["storage", "tabs", "scripting", "activeTab", "sidePanel"],
  "content_scripts": [
    { "matches": ["<all_urls>"], "js": ["content.js"], "run_at": "document_idle" }
  ],
  "icons": { "16": "icons/16.png", "48": "icons/48.png", "128": "icons/128.png" }
}
panel.html — minimum-viable side panel markup·html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>ClipDeck</title>
    <style>
      body { font-family: system-ui, sans-serif; margin: 0; padding: 16px; }
      h1 { font-size: 18px; margin: 0 0 12px; }
      .empty { color: #888; font-style: italic; }
    </style>
  </head>
  <body>
    <h1>ClipDeck — Saved Clips</h1>
    <div id="list" class="empty">No clips yet. Press Ctrl+Shift+K on selected text.</div>
    <script src="panel.js"></script>
  </body>
</html>
panel.js — mount, render, subscribe·javascript
// panel.js — clip read, render, 변경 구독
async function render() {
  const { clips = [] } = await chrome.storage.local.get("clips");
  const list = document.getElementById("list");
  list.className = clips.length === 0 ? "empty" : "";
  list.innerHTML = clips.length === 0
    ? "No clips yet. Press Ctrl+Shift+K on selected text."
    : clips
        .map((c) => `<div><strong>${escapeHtml(c.title || c.url)}</strong><br><small>${escapeHtml(c.text.slice(0, 200))}</small></div>`)
        .join("<hr/>");
}

function escapeHtml(s) {
  return String(s).replace(/[&<>"']/g, (ch) => ({
    "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
  })[ch]);
}

chrome.storage.onChanged.addListener((changes, area) => {
  if (area === "local" && "clips" in changes) render();
});

render();

External links

Exercise

첫 번째 code block 의 manifest 변경 적용 (version 0.6.0, side_panel.default_path: "panel.html", permission 에 "sidePanel"). clipdeck/panel.html 과 clipdeck/panel.js 를 두 번째 / 세 번째 code block 으로 생성. Extension reload. ClipDeck toolbar icon click — popup 여전히 나타나야 함, panel 은 아직 아님 (openPanelOnActionClick 아직 활성화 안 함). 이제 Chrome toolbar 의 side-panel toggle (Chrome 버전에 따라 puzzle-piece menu 나 dedicated side panel icon) click 하고 ClipDeck 선택. Panel 열림. Track 3 exercise 에서 저장한 clip 있으면 보임; 없으면 Ctrl+Shift+K 로 몇 개 저장하고 panel 이 수동 refresh 없이 update 되는지 확인.
Hint
Side-panel toggle 에 ClipDeck 안 보이면 "sidePanel"permissions 에 있고 side_panel.default_path 가 설정됐는지 확인, 다음 extension 완전 reload (chrome://extensions → ClipDeck → reload). Panel 열리는데 빈 page 보이면 panel DevTools (panel 안 어디든 우클릭 → Inspect) 열고 JS error 찾기 — 가장 흔한 게 <script src="panel.js"> 경로 typo. Panel 은 일반 extension page; popup 에 쓰는 같은 DevTools workflow 사용 가능.

Progress

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

댓글 0

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

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