C.W.K.
Stream
Lesson 03 of 05 · published

Context menu — Right-click 을 first-class trigger 로

~12 min · contextMenus, selection, right-click, background

Level 0Extension 입덕
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Keyboard shortcut 은 power user 용. Popup 은 discovery 용. Right-click menu 는 그 사이 모두 — 텍스트 선택했는데 어떻게 저장할지 잘 모르는 user — 용. Lesson 3 가 ClipDeck 을 시끄럽지 안 게 obvious 하게 만드는 menu 추가."

API shape

chrome.contextMenus 가 작고 완전히 SW-side. 세 호출이 거의 모든 거 cover:

  • create({ id, title, contexts, parentId? }) — menu item 추가. Id 반환 (Chrome 이 auto-generate 하게 할 때 유용).
  • update(id, { title?, enabled?, visible? }) — 기존 item 을 재생성 없이 변경.
  • remove(id) — item 삭제.
  • removeAll() — 전체 wipe (install / startup 시 재생성 전에 유용).

Plus click event: chrome.contextMenus.onClicked.addListener((info, tab) => ...). info 가 click 기술 (어느 menu id, 현재 선택 텍스트, page URL); tab 은 click 일어난 tab.

Context — Menu 가 나타나는 곳

contexts 배열이 item 이 menu 에 있을 때 결정:

  • page — 어떤 page background 든 right-click (가장 permissive).
  • selection — user 가 highlight 된 텍스트에서 right-click 할 때만. "Save selection to ClipDeck" 에 완벽.
  • link — anchor 태그에서만.
  • image<img> 요소에서만.
  • video, audio — 그 media 요소에서.
  • editable — input field / textarea 에서만.
  • frame — iframe 안에서 specifically.
  • action — user 가 extension 의 toolbar icon 에서 right-click 할 때 (Chrome 88+).

contexts: ['selection'] 가진 item 은 user 가 실제로 뭔가 select 할 때까지 hidden 유지. ClipDeck 이 useful 한 거 안 할 수 있는 page 에 menu noise 없음.

Install-time 패턴

Context menu 가 Chrome 에 저장되지만 매 install / startup 마다 재생성해야 함. Idiomatic 패턴:

chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.removeAll(() => {
    chrome.contextMenus.create({
      id: 'clipdeck-save-selection',
      title: 'Save "%s" to ClipDeck',
      contexts: ['selection'],
    });
  });
});

Title 의 %s 가 Chrome 이 실제 선택 텍스트 (truncate 된) 로 교체. User 가 generic label 대신 "Save 'service workers idle-evict' to ClipDeck" 봄 — 즉시 읽힘.

Click 처리

onClicked handler 가 SW 에서 돔. 관련 field populate 된 info object 받음:

  • info.menuItemId — 어느 item click. 항상 먼저 확인; 한 listener 가 여러 item 처리 가능.
  • info.selectionText — 선택 텍스트 (selection context 용). ~1024 자까지.
  • info.pageUrl — click 일어난 page.
  • info.linkUrllink context 의 destination URL.
  • info.srcUrlimage / video / audio 의 media URL.
  • info.frameUrl — click 이 iframe 안이었을 때.

ClipDeck save flow 에 SW 가 info.selectionText, info.pageUrl, tab.title 에서 직접 clip 생성 가능 — content script 에 메시지 필요 없음, context menu 가 이미 다 줬으니까.

Nested menu 와 parent id

Item 을 submenu 아래 그룹화하려면 parent item 먼저 생성 (onclick 없이, title 만) 하고 그 id 를 child 의 parentId 로 전달:

  • Parent: { id: 'clipdeck-root', title: 'ClipDeck', contexts: ['selection'] }
  • Child A: { id: 'cd-save', parentId: 'clipdeck-root', title: 'Save selection', contexts: ['selection'] }
  • Child B: { id: 'cd-save-tagged', parentId: 'clipdeck-root', title: 'Save with tag…', contexts: ['selection'] }

Submenu 는 2–3 개 넘는 관련 action 있을 때 도움. 하나만 있으면 flat top-level item 이 더 명확.

Context menu 가 discovery 이야기 완성: speed 위한 keyboard, browsing 위한 popup, in-the-moment intent 위한 right-click. contexts 배열이 useful 안 한 데서 menu 조용. Title 의 %s 가 user 한테 뭘 저장할지 보여 줘.
Chrome 이 multi-extension menu item 그룹화. 세 extension 이 모두 'Save to X' item 추가하면, Chrome 이 결국 single 'Extensions' submenu 아래 그룹화. 그 그룹화 제어 못 함; Chrome 이 item 수와 너비 기반 결정. Menu item 짧게 유지하고 top-level 과 nested 둘 다 잘 읽히도록 명확히 title.

Code

manifest.json — permission 에 contextMenus 추가·json
{
  "permissions": ["storage", "tabs", "scripting", "activeTab", "sidePanel", "contextMenus"]
}
background.js — selection-context save + action-context panel-open·javascript
// background.js — save-selection menu item 설치
function installMenus() {
  chrome.contextMenus.removeAll(() => {
    chrome.contextMenus.create({
      id: "clipdeck-save-selection",
      title: 'Save "%s" to ClipDeck',
      contexts: ["selection"],
    });
    chrome.contextMenus.create({
      id: "clipdeck-open-panel",
      title: "Open ClipDeck side panel",
      contexts: ["action"], // toolbar icon right-click
    });
  });
}

chrome.runtime.onInstalled.addListener(installMenus);
chrome.runtime.onStartup.addListener(installMenus);

chrome.contextMenus.onClicked.addListener(async (info, tab) => {
  if (info.menuItemId === "clipdeck-save-selection" && info.selectionText) {
    const clip = {
      id: crypto.randomUUID(),
      text: info.selectionText.trim(),
      url: info.pageUrl,
      title: tab?.title ?? "",
      savedAt: Date.now(),
    };
    const { clips = [] } = await chrome.storage.local.get("clips");
    await chrome.storage.local.set({ clips: [clip, ...clips] });
    return;
  }
  if (info.menuItemId === "clipdeck-open-panel" && tab?.id) {
    // Note: toolbar icon right-click 에서 side panel 여는 게
    // Chrome 116+ 에선 user gesture 로 카운트; 옛 Chrome 은 reject 가능.
    try { await chrome.sidePanel.open({ tabId: tab.id }); } catch {}
  }
});
background.js — 여러 관련 action 위한 submenu 패턴·javascript
// background.js — 여러 save flavor 가진 nested 'ClipDeck' submenu
function installNestedMenus() {
  chrome.contextMenus.removeAll(() => {
    chrome.contextMenus.create({
      id: "cd-root",
      title: "ClipDeck",
      contexts: ["selection"],
    });
    chrome.contextMenus.create({
      id: "cd-save",
      parentId: "cd-root",
      title: 'Save "%s"',
      contexts: ["selection"],
    });
    chrome.contextMenus.create({
      id: "cd-save-and-tag",
      parentId: "cd-root",
      title: 'Save "%s" + add tag…',
      contexts: ["selection"],
    });
  });
}

External links

Exercise

clipdeck/manifest.json 의 permission 에 "contextMenus" 추가. 두 번째 code block 을 clipdeck/background.js 에 추가. chrome://extensions 에서 extension reload — 중요, permission 추가가 Chrome 의 re-prompt 필요. Wikipedia article 열기, 문단 선택, right-click. Menu 에 Save "<your selection>" to ClipDeck 보여야 함. Click. Side panel 열기 — clip 이 source title 과 URL 와 함께 나타남. ClipDeck toolbar icon right-click 시도 — Open ClipDeck side panel 나타나야 함; click 해서 확인. Bonus: 세 번째 code block 으로 교체해서 nested ClipDeck submenu 보고 두 child 다 나타나는지 확인.
Hint
Menu 가 아예 안 나타나면 permission 인식 안 됨 — chrome://extensions → ClipDeck → Read and modify your data on all websites 스타일 경고가 contextMenus 포함하는지 확인. 가끔 Chrome 이 extension 제거 + 재추가까지 옛 permission set silent 유지; 단순 reload 안 되면 unpacked 디렉토리 제거 후 재 load 시도. Action context 의 Open ClipDeck side panel 이 click 에 throw 하면 Chrome 이 116 보다 옛 거 — 옛 버전이 그 right-click 을 sidePanel.open 의 user gesture 로 안 다룸. Save-selection flow 는 그 제약 없음.

Progress

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

댓글 0

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

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