본문 바로가기
C.W.K.
Stream
Lesson 01 of 05 · published

chrome.action API — Icon / title / badge / popup

~11 min · chrome.action, manifest, badge, icon, popup

Level 0Extension 입덕
0 XP0/56 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"MV3에서 toolbar는 하나로 합쳐졌어. icon 하나, badge 하나, popup 하나, API 하나. 이 lesson은 chrome.action 연장통을 훑어 — 작고, 초점이 뚜렷하고, 뭔가 달라질 때마다 tab 별로 다시 칠할 수 있어."

action 하나로 통일

MV2 때는 개념이 둘로 갈려 있었어:

  • browser_action — 항상 보이는 toolbar icon. 대부분 extension의 기본 모양이었지.
  • page_action — 지금 페이지에 쓸모가 있을 때만 나타나는 icon (북마크 별표를 떠올리면 돼).

MV3는 이 둘을 chrome.action 하나로 합쳤어. toolbar에 자리를 원하는 extension은 manifest에 "action" object를 선언하고, 그러면 Chrome이 기본적으로 모든 tab에 icon을 보여 줘. 옛날 page-action 처럼 특정 페이지에서만 살아 있게 하고 싶으면, SW에서 chrome.action.disable(tabId)enable(tabId)를 그때그때 불러 주면 돼.

Manifest에 뭘 적나

최소한 이 셋이야:

  • action.default_icon — toolbar에 뜨는 icon. 경로 문자열 하나로 줘도 되고, size 별로 key를 나눈 object로 줘도 돼.
  • action.default_title — 마우스를 올렸을 때 뜨는 tooltip.
  • action.default_popup — 눌렀을 때 Chrome이 열어 줄 HTML 파일. SW에서 chrome.action.onClicked로 직접 받고 싶으면 이 줄을 빼면 돼.

icon은 16 / 32 / 48 / 128 픽셀짜리를 준비해. Chrome이 화면 밀도랑 설정을 보고 알맞은 걸 골라 써. size 별 key를 다 채워 주는 게 좋아. 그래야 Chrome이 한 장을 억지로 늘리는 대신 제대로 고르거든.

돌아가는 중에 바꾸기

chrome.action은 SW에서 부를 수 있는 자그마한 조작 창구를 열어 줘:

  • setIcon({ tabId?, path }) — icon을 전체로든 tab 별로든 갈아.
  • setTitle({ tabId?, title }) — tooltip 바꾸기.
  • setBadgeText({ tabId?, text }) — icon 위에 작은 색 딱지를 얹어. 넉 자쯤이 한계라고 보면 돼.
  • setBadgeBackgroundColor({ tabId?, color }) — RGB 배열이나 hex 문자열.
  • setBadgeTextColor({ tabId?, color }) — Chrome 115부터.
  • setPopup({ tabId?, popup }) — 눌렀을 때 열 HTML을 바꿔.
  • disable(tabId?)enable(tabId?) — icon을 흐리게 만들어서 "이 페이지에선 할 게 없어" 를 알려.
  • getUserSettings() — user가 icon을 toolbar에 고정해 뒀는지, 아니면 퍼즐 조각 메뉴 안에 숨겨 뒀는지 읽어.

눈여겨볼 게 하나 있어. 조작 함수 전부가 tabId를 선택적으로 받아. tabId를 주면 그 tab만 바뀌고, 안 주면 따로 지정 안 한 모든 tab의 기본값이 바뀌어.

눌렀을 때 뭐가 일어나나

user가 icon을 누르면 둘 중 하나만 일어나. 둘이 동시에는 안 돼:

  1. Popup 방식 (action.default_popup을 적어 두면 기본으로 이거야). 누르면 popup이 열리고, chrome.action.onClicked는 안 울려.
  2. SW가 받는 방식 (default_popup이 없을 때). 누르면 SW의 chrome.action.onClicked가 울리고, 거기서 뭘 할지 정해 — side panel을 열든, script를 밀어 넣든, 메시지를 보내든.

돌아가는 중에도 둘 사이를 오갈 수 있어. chrome.action.setPopup({ tabId, popup: '' }) 처럼 popup을 빈 문자열로 두면 그 tab의 popup이 꺼지고 onClicked가 다시 살아나.

Badge는 신호 하나만

Badge는 작아. 대부분 넉 자쯤에서 잘리니까 한 가지만 또렷하게 전해:

  • 숫자. 안 읽은 메시지, 오늘 저장한 clip, 밀린 작업.
  • 상태. 모드를 알려 주는 두 글자 ("ON" / "OFF", "REC" / 빈칸).
  • 경고. 봐 줘야 할 게 있을 때 빨간 느낌표 하나.

빈 문자열 (setBadgeText({ text: '' }))을 넣으면 badge가 지워져. ClipDeck은 오늘 저장한 clip 수 (Track 5 Lesson 5) 랑 tab 별 일시정지 상태에 badge를 쓸 거야.

chrome.action이 toolbar에서 쓸 수 있는 말 전부야 — icon, title, badge, popup, click handler. 조작 함수는 하나같이 tabId를 선택적으로 받아서 tab 별로 다르게 굴 수 있고. Badge 에는 문단을 쓰지 마. 신호 하나만 담아.
user가 icon을 고정했는지 확인해 봐. Chrome 91부터 chrome.action.getUserSettings(){ isOnToolbar: boolean }를 돌려줘. false 면 icon이 퍼즐 조각 메뉴 뒤에 숨어 있다는 뜻이야. 처음 설치했을 때 이걸 확인해서 고정하는 법을 알려 주면 꽤 도움이 돼. 고정을 안 해 두면 user는 그런 게 있었다는 것부터 잊어버리거든.

Code

manifest.json — 여러 size icon을 갖춘 action block·json
{
  "manifest_version": 3,
  "name": "ClipDeck",
  "version": "0.8.0",
  "action": {
    "default_title": "ClipDeck — save clips and browse them",
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/16.png",
      "32": "icons/32.png",
      "48": "icons/48.png",
      "128": "icons/128.png"
    }
  },
  "background": { "service_worker": "background.js" },
  "side_panel": { "default_path": "panel.html" },
  "permissions": ["storage", "tabs", "scripting", "activeTab", "sidePanel", "alarms"],
  "content_scripts": [
    { "matches": ["<all_urls>"], "js": ["content.js"], "run_at": "document_idle" }
  ]
}
background.js — clip 수가 움직이는 badge, onChanged로 갱신·javascript
// background.js — 오늘 clip count 와 badge sync 유지
async function refreshBadge() {
  const { clips = [] } = await chrome.storage.local.get("clips");
  const todayStart = new Date();
  todayStart.setHours(0, 0, 0, 0);
  const todayCount = clips.filter((c) => c.savedAt >= todayStart.getTime()).length;

  await chrome.action.setBadgeBackgroundColor({ color: "#1a6bd6" });
  await chrome.action.setBadgeText({
    text: todayCount === 0 ? "" : String(todayCount > 999 ? "999+" : todayCount),
  });
  await chrome.action.setTitle({
    title: todayCount === 0
      ? "ClipDeck — no clips today"
      : `ClipDeck — ${todayCount} clip${todayCount === 1 ? "" : "s"} today`,
  });
}

function scheduleNextMidnight() {
  const next = new Date();
  next.setHours(24, 0, 0, 0);
  chrome.alarms.create("clipdeck-midnight", { when: next.getTime() });
}

chrome.runtime.onInstalled.addListener(() => {
  refreshBadge();
  scheduleNextMidnight();
});
chrome.runtime.onStartup.addListener(() => {
  refreshBadge();
  scheduleNextMidnight();
});
chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name !== "clipdeck-midnight") return;
  refreshBadge();
  scheduleNextMidnight();
});
chrome.storage.onChanged.addListener((c, a) => {
  if (a === "local" && "clips" in c) refreshBadge();
});
background.js — 첫 설치 때 고정 여부 확인하기·javascript
// background.js — user 가 icon pin 했는지 감지 (Chrome 91+)
chrome.runtime.onInstalled.addListener(async (details) => {
  if (details.reason !== "install") return;
  try {
    const settings = await chrome.action.getUserSettings();
    if (!settings.isOnToolbar) {
      console.log("[ClipDeck SW] icon is hidden behind the puzzle-piece menu");
      // 첫 install onboarding 이 여기서 icon pin 법 설명하는 'Welcome' tab 열거나
      // 일회성 notification 표시 가능.
    }
  } catch (err) {
    // getUserSettings 는 Chrome 91+; 옛 Chrome 은 throw, 무시.
  }
});

External links

Exercise

clipdeck/manifest.json을 첫 번째 code block (version 0.8.0)으로 올려. clipdeck/icons/ 밑에 16/32/48/128 픽셀 PNG를 실제로 만들어 둬 — 연습이니까 한 장을 네 번 복사해도 상관없어. badge 로직은 icon 품질 안 따지거든. 두 번째 code block (badge 갱신)을 background.js에 넣고 reload 해. Ctrl+Shift+K로 clip을 하나 저장하면 toolbar icon에 '1' 이 떠야 해. 몇 개 더 저장하면 badge도 따라 올라가고. 날짜가 바뀌면 (아니면 테스트로 todayStart를 한참 과거 날짜로 박아 두면) badge가 비워져. icon에 마우스를 올려서 tooltip이 개수를 말해 주는지도 봐. 여유가 되면 세 번째 code block도 넣고 ClipDeck을 지웠다 다시 설치해서 console 로그를 확인해 봐.
Hint
Storage와 alarm listener는 service-worker top level에 등록해. setBadgeBackgroundColor를 await하는 건 error handling에 좋지만 그게 color 적용 조건은 아니야. Midnight alarm이 없으면 어제 count가 다음 event까지 남을 수 있어.

Progress

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

댓글 0

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

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