본문 바로가기
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/56 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Keyboard shortcut은 power user 용. Popup은 discovery 용. Right-click menu는 그 사이 모두 — 텍스트 선택했는데 어떻게 저장할지 잘 모르는 user — 용. 이 lesson 에서는 시끄럽지 않으면서도 눈에 잘 띄게 해 주는 메뉴를 ClipDeck에 붙일 거야."

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']을 준 항목은 user가 실제로 뭔가를 골라 두기 전까지 안 보여. ClipDeck이 해 줄 게 없는 페이지에서는 메뉴를 어지럽히지 않는 거지.

Install-time 패턴

우클릭 메뉴는 Chrome이 들고 있긴 한데, 설치할 때와 브라우저가 켜질 때마다 다시 만들어 줘야 해. 관행처럼 쓰는 모양은 이거야:

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 — 어느 항목을 눌렀는지야. 항상 이걸 먼저 확인해. listener 하나가 여러 항목을 같이 받거든.
  • 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.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(),
    };
    await mutateLocal(["clips"], ({ clips = [] }) => ({
      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
Context-menu item은 service-worker restart를 넘어 남으니 onInstalled에서 만들고 idempotence를 위해 먼저 removeAll해. Browser startup마다 다시 만들지 마. contextMenus 자체 install warning은 없어. Menu가 안 보이면 service-worker error와 created context를 봐.

Progress

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

댓글 0

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

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