"설치는 최소한으로 받고, 나머지는 필요해질 때 받아. Track 6은 끝까지 연결된 opt-in 흐름 두 개로 닫아. Export Clips는 user가 누를 때 downloads를 달라고 하고, Enable on This Site는 여태 빠져 있던 주소에서 ClipDeck을 쓰고 싶을 때 host 권한을 달라고 해."
두 flow
Track 5 에서는 ClipDeck에 downloads 랑 optional_host_permissions를 적어 두기만 하고 실제로 달라고는 안 했어. 이 lesson에서 둘 다 불을 켤 거야:
- Export Clips — popup 이나 side panel의 버튼이야. 누르면 아직 권한이 없을 때
downloads를 요청하고, 받고 나서 SW를 통해 실제로 내보내. - Enable on This Site — 지금 tab에 content script가 안 도는 경우에만 뜨는 popup 버튼이야 (주소가 content_scripts.matches에 안 걸렸거나 exclude_matches에 들어 있어서). 누르면
https://<지금-host>/*를 host 권한으로 요청하고, 받으면 그 자리에서 content script를 밀어 넣어.
둘이 모양이 같아. 살펴보고 → 물어보고 → 실행하고 → 거절당하면 곱게 물러나기. 워낙 똑같아서 작은 helper 하나로 묶어 둘 값어치가 있어.
ensurePermission helper
popup 이랑 side panel이 같이 쓸 수 있는 함수 하나야:
async function ensurePermission(req) {
const has = await chrome.permissions.contains(req);
if (has) return true;
return chrome.permissions.request(req);
}
'있나 확인하고 없으면 요청하기' 를 한 번의 호출로 접어 버린 거야. 부르는 쪽은 참/거짓만 보고 갈라지면 돼.
'Enable on This Site' 흐름
이쪽이 더 재밌어. 돌아가는 도중에 extension이 닿는 범위를 바꾸는 거니까. 순서는 이래:
- popup이 열리면 지금 tab의 주소를 읽어.
- content script가 이미 들어가 있는지 확인해. ping 메시지를 하나 던져 보고, 받을 listener가 없어서 실패하면 그걸 잡는 거야.
- 이미 돌고 있으면 버튼에 "ClipDeck is active on this site", 아니면 "Enable ClipDeck on this site" 라고 띄워.
- 누르면
https://<host>/*에 대한 host 권한을 요청해. - 허락이 떨어지면 SW 한테
chrome.scripting.executeScript({ target: { tabId }, files: ['content.js'] })로 content script를 넣어 달라고 해. 그 순간부터 이 tab에서 ClipDeck이 살아나. - 새로고침해도 계속 붙어 있게 하려면
chrome.scripting.registerContentScripts로 동적 content script도 같이 등록해. 그러면 같은 host를 다음에 열 때 알아서 들어가.
Persistent dynamic content script
chrome.scripting.registerContentScripts (Chrome 96부터)로 등록해 두면 SW가 evict 돼도, 브라우저를 껐다 켜도 살아남아. 범위는 host 권한을 받은 주소로만 묶이고. 모양은 이래:
await chrome.scripting.registerContentScripts([{
id: 'clipdeck-dynamic-acme',
matches: ['https://acme.com/*'],
js: ['content.js'],
runAt: 'document_idle',
}]);
지금 뭐가 등록돼 있는지 보려면 getRegisteredContentScripts, 빼려면 unregisterContentScripts({ ids: ['...'] })를 써. 'Disable on this site' 를 만든다면 등록을 빼는 것과 chrome.permissions.remove({ origins: ['https://acme.com/*'] })를 둘 다 불러 줘야 해.
Privacy 신뢰 이야기
이 lesson을 끝내면 설치 시점의 ClipDeck은 이렇게 돼:
- 큰 경고 하나 (좁혀 둔 content_scripts.matches), 중간 경고 하나 (tabs), 그리고 아무 말 없이 지나가는 나머지 (storage / sidePanel / contextMenus / scripting / activeTab / alarms).
- 내보내기, 알림, "다른 사이트에서도 켜기" 는 전부 그 기능을 쓰려는 순간에 물어보는 문 뒤에 있어.
- 프라이버시에 예민한 user는 downloads도 추가 host도 끝까지 안 주고서 핵심 기능을 다 쓸 수 있어. 자기가 정말 원하는 확장에만 값을 치르는 거지.
Chrome Web Store 심사자가 보는 기준이 딱 이거야. "설치 경고가 이 extension이 설치 직후 실제로 하는 일에 걸맞나?" 설치할 때 다 달라고 하는 extension은 나중에 그걸 정말 다 쓴다고 해도 이 기준에서 떨어져. 물어보는 시점을 그 기능을 쓰는 순간으로 미룬 extension이 통과하고.
Track 6 마무리
여섯 track이 끝났어. 지금 ClipDeck이 가진 건 이래:
- 권한이 갈래별로 정리된 MV3 manifest.
- service worker, popup, side panel, content script, 우클릭 메뉴, 단축키, 주소창 키워드, badge.
- Create (Track 3)와 Read (Track 4).
- tab 별 멈춤 (Track 5).
- 필요한 순간에 받는 downloads와 host 권한 (이 track).
Track 7에서는 CRUD의 나머지인 Update와 Delete를 채우고, 사람들이 실제로 쓰는 어수선하고 framework로 뒤덮인 사이트에서도 여태 만든 게 제대로 굴러가게 해 줄 DOM 연장통을 붙일 거야.
Dynamic registration lifecycle. http:/https: origin만 받고 registration ID는 raw Base64 말고 안전한 hex/hash alphabet으로 만들어. User가 site를 끄면 unregisterContentScripts를 명시적으로 불러. Registration은 기본적으로 계속 남고, host grant 취소가 injection을 막을 수는 있어도 feature-state cleanup을 대신하지는 않아.