"Track 3은 CRUD의 첫 글자를 찍으면서 끝나. content script가 user의 선택을 잡아서 service worker로 넘기고, SW가 storage에 쓰고, popup이 다시 그려. 고리 하나가 돌면서 진짜 일이 한 조각 처리되는 거야. ClipDeck의 나머지는 전부 이 틀의 변주고."
Clip schema
코드보다 모양부터 정하고 가자. clip 하나는 이렇게 생겼어:
id— string. 저장 시점 생성, 가장 단순하고 올바른 generator는crypto.randomUUID()(Chrome 92+). Id가 Track 7의 U / D 작업 primary key.text— string. 선택 텍스트, trim.url— string. 저장 시점location.href.title— 문자열이야. 저장하는 순간의document.title을 담아. 덕분에 popup이 밋밋한 URL 대신 "From Wikipedia — Service Worker" 처럼 보여 줄 수 있어.savedAt— number.Date.now()밀리초. Newest-first 정렬과 상대 시간 표시에 사용.
chrome.storage.local의 clips 아래에 평범한 배열로 넣어. 항목 다섯 개에 중첩도 없고, 전부 JSON으로 그대로 옮겨져. Track 7의 수정 흐름은 text만 건드리고, 삭제는 id로 빼. Track 4의 side panel도 같은 배열을 정렬하고 거르고.
세 trigger
저장을 시작하는 길이 셋 있고, 각자 뒤쪽 track에서 더 자라:
- 떠 있는 버튼 — Lesson 4에서 이미 붙여 놨어. 어느 페이지에서나 보이니까 찾기가 제일 쉬워.
- 단축키 — 이 lesson의 주인공이야. manifest의
commands에 적어 두면 SW가 그 명령을 받아서, 지금 tab의 content script 한테 메시지를 보내고, content script가 선택 영역을 읽어서 넘겨줘. - 우클릭 메뉴 — Track 5에서 다뤄. 고른 텍스트 위에서 우클릭 → "Save to ClipDeck".
실제로 ClipDeck을 쓰는 사람은 아마 단축키를 주로 쓰게 될 거야 (기본은 Ctrl+Shift+K 고, 원하면 바꿀 수 있어). 우클릭 메뉴는 그런 게 있다는 걸 알려 주는 뒷받침이고. 떠 있는 버튼은 나중에 사이트별로 켜고 끄는 설정으로 자라게 될 거야.
End-to-end flow
- User가 page에서 텍스트 select 하고
Ctrl+Shift+K. - Chrome이 SW에서
chrome.commands.onCommand를 command 이름save-clip으로 fire. - SW가 active tab의 content script에 질문:
chrome.tabs.sendMessage(activeTab.id, { type: 'captureSelection' }). - Content script가
window.getSelection().toString()read, URL과 title 모음, payload로 응답. - SW가 응답 받아, clip object 만들고, 기존
clips를 storage에서 read, append, write back. chrome.storage.onChangedfire. popup (열려 있으면)과 side panel (Track 4)이 re-render.
단계 하나하나가 작아서 따로따로 확인할 수 있어. popup에 clip이 안 보이면 사슬을 거꾸로 되짚어. DevTools에서 storage를 먼저 보고, 그다음 SW 로그, 그다음 content script 로그 순으로.
뭐가 잘못돼
- toolbar를 건드리는 순간 선택이 풀려. toolbar를 누르면 popup이 접히면서 선택이 지워지는 경우가 있어. 단축키로 content script가 바로 잡는 방식은 이걸 피해 가. 반대로 popup 안의 "지금 선택한 거 저장" 버튼은 popup이 열리기 전에 content script가 이미 잡아 뒀어야 해.
- 손댈 수 없는 페이지.
chrome://, Chrome Web Store,view-source:주소에는 content script가 안 들어가. SW에서 content script로 보내는 호출을 try/catch로 감싸고, "이 페이지에선 ClipDeck이 못 움직여" 정도로 친절하게 알려 줘. - 아무것도 안 골랐을 때. content script는 비었다는 걸 분명히 담아서 답해 (
{ ok: false, reason: 'no-selection' }). 알림을 띄울지 그냥 넘길지는 SW가 정하게 두고.
clip 하나에 고리 하나야. content script에서 선택을 잡고, SW로 메시지를 보내고, storage에 쓰고, onChanged를 타고 다시 그려. 앞으로 붙는 ClipDeck 기능은 전부 이 고리에 동사만 바꿔 끼우는 거야.
Wikipedia에서 먼저 해 봐. content script 한테 Wikipedia 만큼 순한 상대가 없어. 화면을 제멋대로 갈아엎지도 않고, CSP도 사납지 않고, 선택을 가로채는 handler도 없거든. SPA (Gmail, Twitter/X) 나 민감한 곳 (은행, 비밀번호 관리자 — 여긴 계속 제외해 두고)을 건드리기 전에, Wikipedia 기사 하나에서 Ctrl+Shift+K부터 popup에 clip이 뜨는 데까지 고리가 다 도는지 확인해.
Compound storage update는 원자적이지 않아.
get → modify → set을 surface 둘이 동시에 돌리면 한쪽 변경이 사라질 수 있어. Service worker를 sole writer로 두고 mutation queue 하나로 직렬화하거나 record를 독립 key로 저장해. 한 번의 set은 그 안의 key를 함께 적용하지만, 앞뒤 read-modify-write 전체가 atomic인 건 아니야.