~15 min · tabs, onUpdated, storage, messaging, service-worker, permissions
Level 0Extension 입덕
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Lesson 4 가 명사. Lesson 5 가 동사. Lesson 6 은 그 둘을 함께 쓰는 작고 진짜인 한 가지 — 실시간으로 올라가는 걸 볼 수 있는 방문 카운터. Track 2 가 여기서 끝나. popup 닫혀 있고 laptop 잠들어 있어도 도는 feature 하나로."
뭘 만들고 있는 거?
방문 카운터는 ClipDeck 의 가장 단순한 R — storage 에서 state read, render. tab load 끝날 때마다 service worker 가 카운터 증가. popup 열리면 total 과 URL 별 breakdown 표시. popup 닫고, SW 잠들고, Chrome 재시작해도 카운트는 살아 — 다 chrome.storage.local 안에 있어서.
이게 Track 2 의 마지막 ClipDeck slice. Track 3 부터는 같은 stack — storage 가 state, message 가 action, event 가 trigger — 이 clip CRUD 자체를 호스팅. Lesson 6 은 stack 이 end-to-end 로 제대로 연결된 걸 증명하는 worked example.
chrome.tabs.onUpdated event
Tab 은 모든 state 변화 — favicon load, title swap, navigation, completion — 마다 onUpdated fire. 우리는 completion 만 관심, 그래서 changeInfo.status === "complete" 와 tab.url 존재 여부로 guard:
tabId — 변화 일어난 tab 의 id.
changeInfo — 이번 fire 에서 변한 것. status (loading / complete) 포함, 가끔 url 도.
tab — 변화 후 전체 tab object. changeInfo.url 보다 tab.url 사용 — 후자가 실제 URL transition 때만 있음.
onUpdated listen 하려면 "tabs" permission (URL read 위해) 또는 "activeTab" permission (user 가 action 부를 때만) 필요. 항상 도는 방문 카운터엔 "tabs" 가 맞아 — manifest.json 의 "storage" 옆에 추가.
System page 거르기
금방 알아챌 거야 — chrome://, about:, chrome-extension:// URL 이 카운트 오염시킴. 건너뛰기 — user 가 의미 있게 page 로 browse 한 게 아냐. 한 줄짜리 guard 가 noise 의 99% 처리:
빈 / null URL 도 건너뛰기 (실제 navigation 도착 전 new-tab transition 에서 나타나). 그 guard 이후 모든 증가는 user 가 시작한 실제 page load 와 mapping.
Schema
chrome.storage.local 의 두 key:
totalVisits: number — monotonic 카운터, user action 외엔 reset 안 됨.
urlCounts: Record<string, number> — URL 별 카운트. popup 에서 top-N list 보여 줄 수 있게 해 줘.
이 schema 가 flat scalar 와 structured object 둘 다 storage 에서 시연하는 가장 작은 것. ClipDeck 의 clip list (Track 3) 도 같은 패턴 — 배열용 key 하나, 파생 카운트 / metadata 용 key 하나.
Popup 쪽
popup 이 load, 두 key read, render. chrome.storage.onChanged 도 구독; SW 가 새 카운트 write 하면 message passing 없이 popup re-render. "Reset counters" 버튼은 { type: "resetVisitCounts" } 를 SW 로; SW 가 storage 에 0 write 하고 {ok:true} 응답. Reset 은 action 이라 message 로 통과 (Lesson 5 의 동사/명사 split).
한 feature 안의 Track 2 전체 stack: event 가 SW 깨우고, SW 가 storage mutate, popup 이 onChanged 로 re-render, user action 이 message 로 돌아옴. ClipDeck 이 이후 모든 feature 에 타고 다닐 loop.
하필 왜 방문 카운터? 새 domain 만들지 안 하고 Track 2 의 모든 개념을 행사하는 가장 작은 feature 라서. 원하면 Track 3 에서 카운터 버려도 됨 — point 는 loop 지 카운트 자체 아냐. 어떤 학생은 ClipDeck 의 영구 "이 사이트 얼마나 자주 가?" widget 으로 유지; 둘 다 괜찮은 선택.
실시간으로 올라가는 거 봐 봐. chrome://extensions → ClipDeck → 'Inspect views: service worker' 로 SW DevTools 열기. Application 탭 → Storage → Extension Storage → 'local'. 이제 page 몇 개 navigate. urlCounts 가 refresh 없이 실시간으로 자라. 전체 pipeline 이 제대로 연결됐는지 확인하는 가장 빠른 sanity check.
clipdeck/manifest.json 을 version 0.3.0 으로 올리고 "permissions": ["storage", "tabs"] 로 설정. 첫 번째 code block 의 chrome.tabs.onUpdated listener 를 clipdeck/background.js 에 (기존 message listener 옆에) 추가. clipdeck/popup.html 에 <div>Total visits: <span id="totalVisits">0</span></div>, <ol id="topUrls"></ol>, <button id="resetBtn">Reset counters</button> 추가. clipdeck/popup.js 의 body 를 두 번째 code block 으로 교체. extension reload. popup 열기 — 카운터 0 이어야 함. popup 닫기. 실제 web page 네다섯 개 (평소 쓰는 사이트들) 열기. popup 다시 열기 — totalVisits 가 page load 수와 맞아야 하고 topUrls 가 그것들 listing. Reset 클릭 — 두 카운터 0, top-URL list 비어, popup 다시 열 필요 없어.
Hint
카운트가 안 움직이면 가장 흔한 원인은 "tabs" permission 누락 — 그게 없으면 tab.url 이 undefined 라 isSkippableUrl 이 true 반환. manifest 편집 후 extension reload. popup 이 0 렌더하는데 SW DevTools 의 Application → Extension Storage 엔 실제 카운트 보이면, popup 의 chrome.storage.local.get 호출이 storage write 와 race — destructure key 이름이 정확히 일치하는지 확인 (totalVisit vs totalVisits 같은 typo 는 조용히 0 으로 default). onChanged 가 fire 하는데 popup 이 re-render 안 하면 listener 가 popup DOM ready 전에 등록된 거 — popup.js 에서 onChanged 먼저 등록, 그 다음 마지막에 render() 호출.
Progress
Progress is local-only — sign in to sync across devices.