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

Side panel 등록 — Manifest + chrome.sidePanel

~12 min · side-panel, manifest, chrome.sidePanel, setOptions, user-gesture

Level 0Extension 입덕
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Manifest 에 선언. SW 에서 manipulate. User gesture 아래에서 open. 세 문장, 한 lesson 안의 세 lesson — 그리고 어느 하나라도 건너뛰면 API 가 조용히 동작 거부하는 세 가지 실제 방법."

Step 1 — Manifest 에 선언

두 field 가 함께 default panel 등록:

  • side_panel.default_path — panel 열릴 때 Chrome 이 load 할 extension 안 HTML file. Panel 존재 자체에 필수.
  • permissions"sidePanel" — SW 에서 chrome.sidePanel.* 호출 필수. 그게 없으면 API 자체가 undefined.

그것만으로 panel 이 Chrome 의 side-panel chooser 에 보임에 충분. User 가 수동 선택 가능; 그 외 wire 안 됨.

Step 2 — SW 에서 behavior 구성

chrome.sidePanel API 가 실제로 쓸 세 method 노출:

  • chrome.sidePanel.setOptions({ tabId?, path, enabled }) — panel HTML 과 enabled 상태 설정, 선택적으로 tab 별. 한 tab target 하려면 tabId 전달; global default 면 생략.
  • chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: boolean }) — toolbar icon click 이 popup 대신 panel 열지 toggle. Icon click 의 popup 과 mutually exclusive: click 이 panel 열거나 popup 열거나.
  • chrome.sidePanel.open({ tabId? | windowId? }) — programmatic panel 열기. User-gesture handler (action click, keyboard command, context menu) 에서 호출해야 함, 아니면 reject.

셋 다 SW 에 살아. Panel 자체는 자기 visibility 구성 안 함 — cross-tab perspective 가진 SW 에 그 권한이 있어.

User-gesture 규칙

chrome.sidePanel.open() 가 엄격한 거. Timer, alarm, webRequest event, user gesture 에서 안 온 content-script 메시지에서 호출하면 Side panel can only be opened by a user gesture 로 reject. 정당한 trigger:

  • chrome.action.onClicked — toolbar icon click. Manifest 에 default_popup 없을 때만 fire.
  • chrome.commands.onCommand — manifest commands block 에 선언된 keyboard shortcut.
  • chrome.contextMenus.onClicked — right-click menu item.
  • 실제 user click 에 응답하는 popup 이나 panel 에서 온 메시지일 때의 chrome.runtime.onMessage — Chrome 이 그걸 gesture-derived event 로 다룸.

이 규칙은 보안 baseline: extension 이 user action 없이 user 얼굴에 panel 띄울 수 없음. 존중해; timeout 으로 gesture fake 시도 안 함.

Two-step per-tab 패턴

ClipDeck 에 최종 side-panel 패턴은 per-tab — 각 tab 이 자기 panel context 받아, 나중에 현재 site 별 clip filter 나 per-tab annotation 유지 가능. Setup:

  1. Manifest 가 default pathsidePanel permission 선언.
  2. SW 가 chrome.tabs.onActivated listen. 매 event 마다 chrome.sidePanel.setOptions({ tabId, path: 'panel.html', enabled: true }) 호출.
  3. SW 가 install/startup 시 chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }) 별도 한 번 호출, toolbar icon click 이 popup 대신 panel 열게 — 그게 원하는 UX 면.

Popup 공존 선택

ClipDeck 은 popup 과 panel 둘 다 ship. Toolbar icon 이 default 로 뭘 열지 골라야 함:

  • action.default_popup: "popup.html" 유지 AND openPanelOnActionClick: false → toolbar click 이 popup 열고, panel 은 자기 toggle 이나 버튼으로만 열림.
  • default_popup 제거 AND openPanelOnActionClick: true 설정 → toolbar click 이 panel 열고; popup 은 icon 에서 닿을 수 없음 (키보드나 다른 flow 로 여전히 열 수는 있음).
  • 둘 다 유지: 불가. Chrome 이 매 click 에 선택 강제.

ClipDeck 현재 선택: popup 을 default 로 유지 (더 빨리 load, "reset / quick stats" 버튼 가짐), popup 에 user-gesture handler 에서 chrome.sidePanel.open() 부르는 "Open clip list" 버튼 추가.

Manifest 가 path 선언. SW 가 per-tab 과 behavior 구성. open() 은 user gesture 필요. 어느 step 이라도 빠지면 panel 이 존재 안 하든, 안 나타나든, misleading error 로 reject.
Silent setOptions race. setOptions({ tabId, path, enabled: true }) 를 너무 일찍 — Chrome 이 tab 등록 끝내기 전 — 호출하면 silently fail. 가장 안전한 패턴은 chrome.tabs.onActivatedchrome.tabs.onUpdated 함께 listen: tab 이 active 되거나 load 끝낼 때마다 setOptions 재적용. 살짝 redundant, ordering 면역.

Code

background.js — activate + load 시 per-tab side panel 등록·javascript
// background.js — per-tab side panel 등록, open-on-action 활성화
chrome.runtime.onInstalled.addListener(async () => {
  // Toolbar-icon click 이 panel 열지 안 함 — popup 이 그 자리 유지.
  await chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false });
});

chrome.tabs.onActivated.addListener(async ({ tabId }) => {
  await chrome.sidePanel.setOptions({
    tabId,
    path: "panel.html",
    enabled: true,
  });
});

chrome.tabs.onUpdated.addListener(async (tabId, info) => {
  if (info.status !== "complete") return;
  await chrome.sidePanel.setOptions({
    tabId,
    path: "panel.html",
    enabled: true,
  });
});
popup.js — 실제 user click 에서 side panel 열기·javascript
// popup.js — Open Panel 버튼 (popup 안에 살아, 그게 user gesture)
async function openSidePanel() {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  if (!tab) return;
  // 이 handler 안에서 sidePanel.open 호출 OK — popup click 이
  // user gesture, Chrome 이 이 호출로 그걸 carry.
  await chrome.sidePanel.open({ tabId: tab.id });
  // Popup 이 보통 이 후 auto-close; 예상된 동작.
}

document.getElementById("openPanelBtn").addEventListener("click", openSidePanel);
background.js — 키보드 command 가 panel 열기·javascript
// background.js — panel 여는 keyboard command 도 bind
chrome.commands.onCommand.addListener(async (command) => {
  if (command !== "open-panel") return;
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  if (!tab?.id) return;
  await chrome.sidePanel.open({ tabId: tab.id });
});

// Manifest commands block 에 save-clip 옆에 추가:
//   "open-panel": {
//     "suggested_key": { "default": "Ctrl+Shift+P", "mac": "Command+Shift+P" },
//     "description": "Open the ClipDeck side panel"
//   }

External links

Exercise

첫 두 code block 을 clipdeck/background.js 와 clipdeck/popup.js 에 각각 추가. clipdeck/popup.html 에 <button id="openPanelBtn">Open Clip List</button> 추가. Extension reload. Toolbar icon click — popup 나타남. Open Clip List click — side panel 이 clip 과 함께 열림. 다른 tab 으로 전환, side-panel toggle click 해서 panel 이 같은 content 로 다시 열리는지 확인. 선택적으로 세 번째 code block (keyboard command) 와 매칭 manifest entry 추가, Ctrl+Shift+P 테스트. 확인: panel 열림; SW console 에서 setTimeout 안에 chrome.sidePanel.open 호출 시도 (SW DevTools 에 그냥 타이핑) 하면 user-gesture error 로 reject — 그게 존중해야 할 보안 경계.
Hint
Popup 의 Open Clip List 버튼이 panel 안 열면 popup DevTools (popup 우클릭 → Inspect → Console) 에서 Side panel can only be opened by a user gesture 확인 — 보통 open() 호출 전 다른 거 await 해서 Chrome 이 gesture context 잃은 경우. Fix 는 handler 에서 sidePanel.open() 을 먼저 호출, 다른 work 는 그 후. onActivated 가 예상대로 fire 안 하면, extension load 시 active tab 은 're-activate' 안 됨 — 현재 active tab 에 setOptions 호출하는 startup-time pass 필요할 수 있음. tabs.onUpdated 경로가 실용에서 이걸 cover.

Progress

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

댓글 0

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

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