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/54 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 에 스크립트 list. Chrome 이 install 시점에 그 list 읽고 매칭되는 모든 navigation 에 inject. background.js 코드 없음, manifest 가 이미 선언한 것 외에 permission prompt 없음, tab 별 결정 없음. 그냥 "이 스크립트는 이 URL 들에 돈다."

각 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 가 아닌 때 돌아야 할 때.
  • return value 원할 때 — executeScript 가 inject 한 함수 결과를 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 <all_urls> + 민감 site 용 fallback 으로 activeTab — 이게 canonical ClipDeck shape.

Declarative 는 "이 URL 들에 항상." Programmatic 은 "지금, 내 선택으로, 이 특정 tab 에." 헷갈리면 물어 봐: "이 스크립트가 이미 present 해야 해, 아니면 user 가 부탁할 때 inject 해도 돼?"
<all_urls> install 경고. "matches": ["<all_urls>"] 인 manifest 는 Chrome 의 "모든 웹사이트의 데이터를 read 하고 change" install prompt — user 가 보는 가장 무서운 거 — 를 trigger. 진짜 필요할 때만 사용. 가능하면 좁은 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

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

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