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

Declared vs optional permission — 두 grant 순간을 실용으로

~12 min · permissions, optional_permissions, chrome.permissions, user-gesture

Level 0Extension 입덕
0 XP0/56 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Declared permission은 입장료. Optional permission은 나중에 파는 upgrade. Lesson 2가 chrome.permissions API가 요청 defer 하고, feature gate 하고, '아니' 에서 복구하게 해 주는 법."

chrome.permissions API

네 method가 runtime 이야기 cover:

  • chrome.permissions.contains({ permissions?, origins? }) — extension이 현재 이것들 가졌어? Boolean 반환. 저렴, sync 느낌 (Promise-based 지만 즉시 resolve).
  • chrome.permissions.request({ permissions?, origins? }) — user 한테 지금 이것들 grant 요청. user-gesture handler 안에서 호출 필수. Grant 면 true, deny 나 dismiss 면 false 반환.
  • chrome.permissions.remove({ permissions?, origins? }) — 이전에 가졌던 permission drop. User가 이전에 grant 한 upgrade 회수하게 해 주는 데 유용.
  • chrome.permissions.getAll() — full snapshot. { permissions: string[], origins: string[] } 반환.

Plus 변경 event: chrome.permissions.onAdded.addListenerchrome.permissions.onRemoved.addListener. Polling 없이 grant / 회수에 반응 원하면 SW에서 구독.

User-gesture 제약

chrome.permissions.request가 Chrome이 user gesture로 인정하는 handler 안에서만 동작:

  • Popup / side panel / options page 요소의 click handler.
  • chrome.action.onClicked, chrome.commands.onCommand, chrome.contextMenus.onClicked.
  • User-gesture-derive 된 popup click 으로부터의 message (transitive).

timer 안에서든, alarm 안에서든, tab event 안에서든, user가 뭘 누른 게 아닌 자리에서 부르면 This function must be called during a user gesture. 하고 거절당해. 흉내 내려 들지 마. 그 거절은 버그가 아니라 보안 장치야.

Two-step UX 패턴

Optional-permission-gated feature의 깔끔한 shape:

  1. Feature의 UI 정상 렌더 (버튼 / menu item / link).
  2. Click 시 먼저 chrome.permissions.contains 확인. 이미 grant 면 feature 실행.
  3. Grant 안 됐으면 chrome.permissions.request 호출. Chrome prompt 나타남.
  4. User가 grant 하면 feature 실행 진행.
  5. user가 거절하면 그 자리에 친절하게 한 줄 띄워 줘 — "내보내려면 Downloads 권한이 필요해. 허락하려면 다시 눌러 봐." 그리고 그 기능 버튼은 계속 보이게 둬.

이 shape이 대칭적이고 용서적. User가 언제든 회수 가능 (Chrome이 chrome://extensions/?id=... 에 Permission 탭 표시), 같은 flow 다시 trigger 해 re-grant.

Request가 사는 곳

handler는 user가 뭘 눌렀다는 걸 아는 자리라면 어디든 둬도 돼. 흔히 두는 데가 셋 있어:

  • Popup 버튼 — popup.js click handler. Popup이 대부분 chrome.permissions.request flow에 auto-close, grant 후 계속해야 하면 popup 다시 열기.
  • Options page 버튼 — 보통 opt-in feature 구성의 home. Settings page가 request 동안 화면에 persist, 가장 부드러운 UX.
  • side panel 버튼 — popup 이랑 똑같이 동작하는데, 물어보는 창이 뜬 뒤에도 안 닫히고 남아 있어.

SW가 request 직접 시작 못 함 (SW에 user gesture 없음), 하지만 그 surface 중 하나에서 메시지 받고 surface가 grant 확인 후 downstream work trigger CAN.

Optional host 생김새

optional_host_permissions가 URL pattern에 같은 방식 동작. ClipDeck이 install 시 user가 가입 안 한 site에 inject 필요할 때 사용. 예: popup의 "이 site에서 ClipDeck 허용" 버튼이 https://<current-host>/* 요청. Chrome이 "ClipDeck이 이 site의 데이터 read와 change 허용?" dialog 표시. User가 Allow click 후부터 content script가 거기 auto-inject.

Declared = essential, install 시 지불. Optional = nice-to-have, user가 손 뻗을 때 지불. chrome.permissions.contains + chrome.permissions.request + graceful 'deny' 경로 가 full UX.
물어보는 순간 popup이 닫히는 함정. 많은 Chrome 버전에서 권한 창이 뜨면 popup이 같이 닫혀. 흐름을 "누른다 → 물어본다 → 기능을 돌린다" 로 짜 뒀다면 마지막 단계가 아예 안 돌아. popup이 이미 사라졌으니까. 그러니 실제 일은 SW로 넘기거나 (허락받은 뒤 popup이 메시지를 보내는 식으로), 안 닫히는 side panel로 옮겨서 허락 이후 동작이on 닿게.

Code

popup.js — permission 확보 후 SW에 work 위임·javascript
// popup.js — chrome.permissions.request 로 feature-gated
async function ensureDownloadsPermission() {
  const has = await chrome.permissions.contains({ permissions: ["downloads"] });
  if (has) return true;
  return chrome.permissions.request({ permissions: ["downloads"] });
}

document.getElementById("exportBtn").addEventListener("click", async () => {
  const ok = await ensureDownloadsPermission();
  if (!ok) {
    document.getElementById("exportStatus").textContent =
      "Downloads permission denied. Click Export again to retry.";
    return;
  }
  // 실제 export trigger. Popup 이 prompt 에 닫힐 수 있어, 더 안전한 패턴은
  // SW 한테 메시지 보내고 다운로드 소유하게.
  await chrome.runtime.sendMessage({ type: "exportClips" });
  document.getElementById("exportStatus").textContent = "Exporting…";
});
background.js — popup이 grant 확인 후 SW가 chrome.downloads 소유·javascript
// background.js — SW 가 실제 chrome.downloads 호출 처리
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message?.type !== "exportClips") return;
  (async () => {
    const has = await chrome.permissions.contains({ permissions: ["downloads"] });
    if (!has) {
      sendResponse({ ok: false, reason: "no-downloads-permission" });
      return;
    }
    const { clips = [] } = await chrome.storage.local.get("clips");
    const json = JSON.stringify(clips, null, 2);
    const url = `data:application/json;base64,${btoa(unescape(encodeURIComponent(json)))}`;
    await chrome.downloads.download({
      url,
      filename: `clipdeck-export-${new Date().toISOString().slice(0, 10)}.json`,
      saveAs: true,
    });
    sendResponse({ ok: true, count: clips.length });
  })();
  return true;
});
background.js — live UI 반응성 위해 onAdded / onRemoved listen·javascript
// background.js — user 가 어떤 permission grant 나 revoke 할 때 반응
chrome.permissions.onAdded.addListener((perms) => {
  console.log("[ClipDeck SW] permissions granted:", perms.permissions, perms.origins);
});

chrome.permissions.onRemoved.addListener((perms) => {
  console.log("[ClipDeck SW] permissions revoked:", perms.permissions, perms.origins);
  // 예: downloads 가 revoke 됐으면 popup 의 Export 버튼 disable
  // (popup 이 매 open 시 chrome.permissions.contains 어쨌든 re-read 함).
});

External links

Exercise

clipdeck/popup.html에 id exportBtnExport Clips 버튼과 status div exportStatus 추가. 첫 번째 code block을 clipdeck/popup.js에, 두 번째를 clipdeck/background.js에 추가. Reload. Popup 열기, Export Clips click. Chrome이 'Add Downloads' prompt 표시. Allow click — clip export의 Save dialog 나타남. Popup 다시 열기. permission이 이제 grant 됐으니, 두 번째 export는 one click. 다음 chrome://extensions/?id=<ClipDeck id> → Permissions 탭, Downloads revoke, 다시 시도 — prompt 다시 나타남. Deny 시 popup status 메시지 보이는지 확인.
Hint
chrome.permissions.requestThis function must be called during a user gesture 로 reject 하면, user gesture context 잃은 것 — 보통 request 호출 전 다른 거 await 해서. Handler에서 request를 먼저 호출, 다른 work는 그 후. chrome.downloads.download 가 prompt가 Allow 보여줬는데도 permission not granted error 면 SW의 permission snapshot이 stale — SW handler 시작에서 chrome.permissions.contains 다시 호출하면 현재 grant read. Popup-close 함정이 여기서 물어. popup.js의 request 뒤에 'continue export' 로직을 두지 마. 앞에서 보여 준 대로 SW로 옮겨.

Progress

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

댓글 0

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

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