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

들어가는 두 방식 — Declarative injection vs chrome.scripting

~12 min · content-script, injection, manifest, chrome.scripting, permissions

Level 0Extension 입덕
0 XP0/56 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Chrome이 매 매칭 URL 마다 자동으로 inject 하든 (declarative), service worker가 흥미로운 일 생긴 순간 inject 하든 (programmatic). Lesson 2는 들어가려는 방에 맞는 문 고르기."

Declarative — manifest가 그렇게 말함

더 단순한 길부터 보자. manifest.jsoncontent_scripts에 스크립트를 적어 두는 거야. 그러면 Chrome이 설치할 때 그 목록을 읽어 두고, 조건에 맞는 페이지로 갈 때마다 알아서 넣어 줘. background.js에 쓸 코드도 없고, manifest에 이미 적어 둔 것 말고 추가로 물어보는 것도 없고, tab 마다 판단할 일도 없어. 그냥 "이 스크립트는 이 주소들에서 돈다" 하나야.

각 entry가 logical content script 하나. 실제로 쓸 field 들:

  • matches — 필수. URL match pattern 배열 (https://*.github.com/*, <all_urls>, 등).
  • exclude_matches — 선택, matches에서 빼낼 영역.
  • js — extension package 안 script file 배열.
  • css — stylesheet file 배열. 같은 방식으로 inject.
  • run_atdocument_start (DOM parse 전), document_end (DOM ready, resource는 로딩 중), document_idle (다 settle — default).
  • all_frames — boolean, default false. iframe 안에도 닿아야 하면 true.
  • worldISOLATED (default, Lesson 3에서 다룸) 또는 MAIN (Chrome 111+, page 자체 JS world에서 실행).

Programmatic — chrome.scripting.executeScript

On-demand 경로: manifest에 스크립트 list 안 하고, 뭔가 trigger 되면 service worker가 inject. Toolbar icon 클릭, context menu activation, popup 메시지, alarm fire 등 — SW가 chrome.scripting.executeScript 부르고 함수나 파일을 특정 tab에 push.

이게 맞는 도구일 때:

  • 스크립트가 가끔만, 모든 page load가 아닌 때 돌아야 할 때.
  • inject한 함수의 return value가 SW로 돌아와야 할 때.
  • Manifest가 표현 못 하는 logic 기반으로 어느 tab / 어느 frame 인지 선택해야 할 때.

대가는 "scripting" permission과 만지려는 URL의 host permission — permission 모델은 Track 6가 풀어 줘.

activeTab 단축키

현재 tab의 toolbar-click injection 에는 "activeTab" 이라는 특별 permission이 있어 — user prompt 없이 임시 host access 부여. User가 toolbar icon 클릭 (또는 context menu / keyboard shortcut trigger) 시 활성화, navigation까지 그 tab에 유지. Opt-in extension의 Chrome 권장 경로 — 무서운 <all_urls> 경고 없이 "지금 있는 페이지에 부탁하면 동작".

둘 결합

둘 다 같이 쓰는 게 완전히 valid 하고 흔해. ClipDeck의 최종 shape:

  • Declarative — 항상 도는 선택 capture: 매 page, user 선택 listening, 저장 요청 시 즉시 보낼 수 있게.
  • Programmatic — 일회성 작업: " page의 article body 추출," " page에 저장된 clip highlight," toolbar icon 이나 popup 버튼으로 trigger.

ClipDeck Track 3 선택

Track 3 milestone — user 선택 텍스트 저장 — 에는 declarative가 승리. 스크립트가 매 page에서 listening 해야 user가 save action 부르는 순간 selection이 이미 scope에 있어. Programmatic injection은 user의 intent와 race 하게 됨: SW가 inject 끝낼 무렵 selection이 사라졌을 수 있어 (toolbar 클릭이 popup 접고 가끔 selection clear). 넓은 declarative match와 activeTab programmatic path는 같은 정적 script의 fallback 관계가 아니라 별도 전략이야. Declarative entry에서 뺀 site라면 user gesture 뒤 chrome.scripting.executeScript를 부를 때만 activeTab이 도와.

Declarative는 "이 URL 들에 항상." Programmatic은 "지금, 내 선택으로, 이 특정 tab에." 헷갈리면 물어 봐: "이 스크립트가 이미 present 해야 해, 아니면 user가 부탁할 때 inject 해도 돼?"
<all_urls> install 경고. "matches": ["<all_urls>"] 인 manifest는 Chrome이 "모든 웹사이트의 데이터를 읽고 변경"한다는 강한 install prompt를 띄워. user가 가장 경계하는 경고야. 진짜 필요할 때만 사용. 가능하면 좁은 pattern (https://*.github.com/*) 이나 activeTab + programmatic injection 선호. Chrome Web Store review도 명확한 정당화 없는 넓은 host permission에 push back.

Code

manifest.json — 합리적 exclusion 가진 declarative content script·json
{
  "manifest_version": 3,
  "name": "ClipDeck",
  "version": "0.4.1",
  "action": { "default_popup": "popup.html" },
  "background": { "service_worker": "background.js" },
  "permissions": ["storage", "tabs", "scripting", "activeTab"],
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "exclude_matches": [
        "https://accounts.google.com/*",
        "https://*.bank.com/*"
      ],
      "js": ["content.js"],
      "run_at": "document_idle",
      "all_frames": false
    }
  ],
  "icons": { "16": "icons/16.png", "48": "icons/48.png", "128": "icons/128.png" }
}
background.js — page snapshot 반환하는 programmatic injection·javascript
// background.js — toolbar click 시 programmatic injection
chrome.action.onClicked.addListener(async (tab) => {
  if (!tab.id) return;
  const [result] = await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: () => {
      // Page 안에서 돔 (default 는 isolated world).
      // 값을 SW 로 return.
      return {
        url: location.href,
        title: document.title,
        wordCount: document.body.innerText.split(/\s+/).length,
      };
    },
  });
  console.log("[ClipDeck SW] page snapshot:", result.result);
});

// Note: chrome.action.onClicked 은 manifest.action 에 default_popup 이
// 선언 안 됐을 때만 fire. Popup 케이스에서는 popup.js 가 SW 로 메시지
// 보내고, SW 가 잡은 tab id 로 chrome.scripting.executeScript 호출.
Filename으로 programmatic injection (script가 extension 안에 있어야 함)·javascript
// Inline function 대신 외부 파일 inject
await chrome.scripting.executeScript({
  target: { tabId: tab.id, allFrames: false },
  files: ["injected.js"],
  world: "ISOLATED", // 또는 "MAIN" 으로 page 의 JS world 에서 실행
});

External links

Exercise

clipdeck/manifest.json을 첫 번째 code block (version 0.4.1, scripting + activeTab permission 포함, 넓은 content_scripts block)으로 update. 두 번째 code block의 toolbar-click handler를 clipdeck/background.js에 추가. **중요:** chrome.action.onClicked은 default_popup 선언이 없을 때만 fire. 이 exercise를 위해 manifest의 action object에서 "default_popup": "popup.html" 을 잠시 제거, icon 클릭이 popup 열기가 아닌 onClicked fire 하도록. Reload. 실제 page 아무거나 열고 ClipDeck toolbar icon 클릭. SW DevTools console에 url / title / word count 가진 snapshot 로깅돼야 함. 다시 전환: default_popup 복원, reload, popup 정상 다시 열리는지 확인.
Hint
chrome.scripting is undefined 뜨면 "scripting" 을 permission에 추가하고 extension reload 잊은 거. executeScriptCannot access contents of the page throw 하면 host permission 누락 — activeTab 자체는 user가 방금 toolbar 클릭했을 때의 active tab만 cover. 임의 URL access 엔 host_permissions 또는 content_scripts.matches 에 URL pattern 필요. SW DevTools console에 아무것도 안 보이면 SW가 evict 됐는데 listener가 너무 늦게 등록된 거 — chrome.action.onClicked.addListener(...) 가 background.js top level에 있고 async function 안이 아니어야 함.

Progress

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

댓글 0

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

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