"Service worker는 browser 안에 살고. Popup은 toolbar에 살고. Content script는 ClipDeck의 조각 중 user가 읽고 있는 페이지 안에 실제로 사는 유일한 거. Track 3가 그 방에 걸어 들어가 불 켜."
세 개의 거실
Track 2를 마칠 무렵 ClipDeck이 사는 방은 둘이었어. event를 기다리는 service worker와, 잠깐 떴다 사라지는 toolbar popup. 둘 다 extension이 소유한 자리지. 그리고 둘 다 github.com의 DOM 에도, user가 Wikipedia에서 긁어 놓은 문단에도, Hacker News에서 마우스를 올려 둔 버튼에도 손이 안 닿아.
그 access는 세 번째 방에만 있어: content script. host tab 안에서 page 자체 JavaScript와 나란히 도는, DOM read/write 권한 풀로 가진 context. Chrome extension이 자기 소유 아닌 페이지 안으로 손을 뻗는 방법.
뭘 할 수 있어?
실용 예 — content script가 가능케 하는 것들:
- User의 선택 텍스트 read (ClipDeck의 Track 3 milestone).
- 모든 페이지에 떠 있는 버튼이나 sidebar inject.
- 요소 restyle — 다크 모드 없는 사이트에 다크 모드, 광고 hide, 쿠키 배너 접기.
- 페이지 단의 click / hover / 키보드 단축키 listen.
- 구조화 데이터 (가격표, 본문) scrape, SW로 보내 storage에 저장.
- Form submit 전 수정 — autofill / validation / sanitization.
페이지가 뜰 때 DOM 안에 실제로 있어야 되는 일은 전부 여기 몫이야. SW도, popup도, side panel도 이걸 대신해 줄 수 없어.
Isolation 거래
Content script가 공짜는 아냐 — DOM access의 대가를 치러. 'isolated world' (Lesson 3에서 다룸) 안에 살아: 같은 DOM, page 자체와는 분리된 JavaScript heap. Host page가 정의한 window.myLib가 content script에 invisible. 스크립트의 console.log는 content-script console로 가지, page console로 안 감. 두 world가 DOM event와 message channel 통해서만 대화.
이 격리가 있어서 content script를 믿을 수 없는 아무 페이지에나 넣어도 괜찮은 거야. 페이지가 우리 함수를 몰래 바꿔치기할 수 없고, 우리 스크립트가 페이지의 전역을 실수로 뭉개지도 못해. 이 경계는 일부러 날카롭게 그어 놓은 거고.
Storage까지 닿는 법
Content script는 chrome.* API의 subset 가짐 — service worker와 대화하기엔 충분하지만 self-contained extension이 되기엔 부족. 가능한 것들:
- 메시지 보내기:
chrome.runtime.sendMessage. - 메시지 받기:
chrome.runtime.onMessage. - Extension storage read/write:
chrome.storage.local(SW가 read 하는 같은 storage).
등록 불가: chrome.tabs.onUpdated, 새 탭 열기, 대부분의 extension-control API. 그건 SW 영역. 실용 ClipDeck 패턴: content script가 DOM (selection, page metadata) read, package, message로 SW에 보내고, SW가 storage에 write. Storage의 system-of-record를 만지는 유일한 context는 SW. content script는 센서.