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

Per-tab vs global side panel — 두 scoping 모델

~12 min · side-panel, setOptions, per-tab, global, tabs

Level 0Extension 입덕
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"같은 panel, 다른 content — per-tab. 같은 panel, 같은 content, 모든 tab — global. ClipDeck 은 둘 다 동시에 원하고, 어느 호출이 이기는지 이해하면 API 가 깔끔히 그걸 허용."

두 scoping 층

chrome.sidePanel.setOptions 의 매 호출이 두 scope 중 하나로 state 설정:

  • Per-tab{ tabId: T, ... } 로 호출. Settings (path, enabled) 가 tab T 가 active 일 때만 적용. 각 tab 이 자기 독립 state 가짐.
  • GlobaltabId 없이 호출. Settings 가 명시적 per-tab override 없는 모든 tab 의 default 됨.

Per-tab 이 global 보다 이김. Tab T 가 setOptions({ tabId: T, enabled: false }) 가지고 global 이 enabled: true 면, panel 은 T 에서 disabled, 다른 모든 곳 enabled.

ClipDeck 이 쓰는 것

ClipDeck 은 clip library 하나 (global storage) 가져, panel content 는 근본적으로 tab 간 같음. 그러나 두 per-tab behavior 가 중요:

  • Site 별 filtered view. User 가 github.com 에 있을 때, panel 이 github.com 에서 저장된 clip 만 보이게 pre-filter 가능. SW 의 tab-update handler 에서 tab.url read 하고 panel path 에 다른 query string write: panel.html?host=github.com.
  • 민감 site 에서 disable. Banking, password manager, 또는 specific user-blocklisted URL 에서 그 tab 의 enabled: false 설정해 panel chooser 가 ClipDeck 숨김. User 의 privacy 기대 인정.

Default (global) state 는 enabled: true, path: 'panel.html' 유지, panel 이 per-tab work 없이 다른 모든 tab 에서 가능.

setOptions 가 합성되는 법

Path field 는 tab 별 다를 수 있음 — 위의 site-filtered view 에 유용. Chrome 이 panel.html?host=github.com 를 navigation 으로 다뤄; panel JS 가 mount 시 new URLSearchParams(location.search) read 하면, 렌더된 list 그에 맞춰 filter 가능. Path 바뀔 때 panel reload.

Enabled field 는 chooser 의 visibility 만 제어 — tab 의 panel disable 한다고 다른 tab 의 이미 열린 panel 안 닫음. Panel 이 disabled 된 tab 으로 전환하면 chooser 에서 ClipDeck 숨겨짐; 다시 돌아오면 다시 노출.

Activation lifecycle

setOptions 호출 trigger 해야 할 tab event:

  • chrome.tabs.onActivated — user 가 이 tab 으로 전환.
  • chrome.tabs.onUpdated with status === 'complete' — page load 끝남; URL 이 이제 안정적으로 read 가능.
  • chrome.tabs.onCreated — 새 tab 열림, 명시적으로 global default 상속 원할 수도.

chrome.runtime.onInstalled 에서 모든 기존 tab 에 setOptions 호출할 필요 없음 — global setOptions 가 cover. 하지만 global 호출 한 번은 필요 — 아직 구체적으로 처리 안 한 tab 에 panel 이 합리적 default 가지게.

Race-free 패턴

결합된 onActivated + onUpdated 패턴이 실용에서 race-free:

  1. Install/startup 시 global setOptions({ path: 'panel.html', enabled: true }) 호출.
  2. 매 onActivated 와 매 onUpdated(complete) 마다 tab.url 에서 원하는 per-tab state 계산하고 setOptions({ tabId, path, enabled }) 호출.
  3. 어떤 tab 이든 첫 event 가 global default 를 per-tab specific 으로 "upgrade". 이후 event 가 idempotent 하게 재적용.

Idempotency 가 중요 — 두 event 가 가까이 fire 가능 (새 tab 열고 navigate, 다른 tab click 후 돌아오기 — SW 가 빠른 sequence 받음). 같은 값으로 setOptions 재적용 저렴.

Per-tab 이 global 보다 이김. Default "always on" 에 global, 특화 (filter / disable / path swap) 에 per-tab. Race-free behavior 위해 onActivated 와 onUpdated 둘 다에서 재적용.
User 한테 이유 설명 없이 panel disable 안 함. 특정 site 의 chooser 에서 ClipDeck silently 숨기는 건 혼란스러움. Banking URL 같은 데서 disable 해야 한다면, 작은 popup 이나 일회성 toast 렌더해 설명: "ClipDeck 이 privacy 위해 이 site 에서 pause." 안 그러면 user 가 ClipDeck 깨진 줄 알고 uninstall.

Code

background.js — global + per-tab side-panel 구성·javascript
// background.js — global default + per-tab 특화
const BLOCKLIST = [
  /^https:\/\/.*\.bank\.com\//i,
  /^https:\/\/accounts\.google\.com\//i,
];

async function configurePanelForTab(tab) {
  if (!tab?.id || !tab.url) return;
  const blocked = BLOCKLIST.some((re) => re.test(tab.url));
  if (blocked) {
    await chrome.sidePanel.setOptions({
      tabId: tab.id,
      enabled: false,
    });
    return;
  }
  const host = new URL(tab.url).host;
  await chrome.sidePanel.setOptions({
    tabId: tab.id,
    path: `panel.html?host=${encodeURIComponent(host)}`,
    enabled: true,
  });
}

chrome.runtime.onInstalled.addListener(async () => {
  // Global default — 아직 per-tab 호출 안 받은 tab cover.
  await chrome.sidePanel.setOptions({ path: "panel.html", enabled: true });
  await chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false });
});

chrome.tabs.onActivated.addListener(async ({ tabId }) => {
  const tab = await chrome.tabs.get(tabId);
  await configurePanelForTab(tab);
});

chrome.tabs.onUpdated.addListener(async (tabId, info, tab) => {
  if (info.status !== "complete") return;
  await configurePanelForTab(tab);
});
panel.js — path query string 에서 parse 된 host filter·javascript
// panel.js — mount 시 URL 에서 host filter read
async function getFilteredClips() {
  const params = new URLSearchParams(location.search);
  const host = params.get("host");
  const { clips = [] } = await chrome.storage.local.get("clips");
  if (!host) return clips;
  return clips.filter((c) => {
    try {
      return new URL(c.url).host === host;
    } catch {
      return false;
    }
  });
}

async function render() {
  const clips = await getFilteredClips();
  const params = new URLSearchParams(location.search);
  const host = params.get("host");
  document.getElementById("filterLabel").textContent = host
    ? `Showing clips from ${host}`
    : "All clips";
  // ...전과 같이 DOM 에 `clips` 렌더...
}

chrome.storage.onChanged.addListener((c, a) => a === "local" && "clips" in c && render());
render();

External links

Exercise

clipdeck/background.js 의 panel 등록을 첫 번째 code block 으로 교체. clipdeck/panel.html 의 body 맨 위에 <div id="filterLabel"></div> 추가하고 panel.js 를 두 번째 code block 의 filter logic 으로 update. Reload. 세 다른 site (wikipedia, github, news.ycombinator.com) 에 clip 저장. Tab 간 전환 — panel header 가 Showing clips from wikipedia.org 등으로 update 되고, 그 host 매칭 clip 만 렌더돼야 함. Blocklisted URL (Chrome Web Store 시도) 로 전환 — ClipDeck 이 side-panel chooser 에서 사라져야 함. 실제 page 로 다시 — 다시 나타남.
Hint
Tab 전환 시 panel refresh 안 되면 path query string 이 실제로 안 바뀌는 것 — configurePanelForTab 가 onActivated 에 돌고 있는지 맨 위에 console.log('reconfiguring', tab.id, tab.url) 추가해 확인. 맞는 site 에서도 filter 가 clip 0 보이면, clip 의 url 이 subdomain 때문에 tab.url 과 다른 host 일 수도; 더 부드러운 UX 원하면 second-level domain 비교로 filter 느슨하게. Blocklisted-URL 테스트: 열린 panel 이 아닌 chooser entry 가 사라지는지 확인 — 이미 열린 panel 은 per-tab disable 에 auto-close 안 됨.

Progress

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

댓글 0

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

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