"Lesson 4의 storage는 worker가 적어 두는 수동적 memory 였어. Message는 context 간 능동적 wake-up signal. Lesson 5는 popup과 service worker 사이의 줄 — 짧고 팽팽하고 한 turn에 정확히 한 번만 당겨지는."
같은 channel의 두 반쪽
모든 Chrome extension이 모든 context에서 같은 runtime channel 노출. 보내려면: chrome.runtime.sendMessage. 받으려면: chrome.runtime.onMessage.addListener. "popup messaging" 따로, "SW messaging" 따로 — 그런 API 없어. 두 context가 같은 두 반쪽 사용.
sendMessage(message, callback?)— runtime으로 메시지를 쏴. MV3 에서는 callback을 안 주면 Promise를 돌려주고. 이 extension 안에 걸려 있는 다른 listener 들이 전부 받아 봐.onMessage.addListener((message, sender, sendResponse) => boolean | undefined)— handler 등록, 들어오는 모든 message에 호출.sendResponse를 async로 부를 거면true반환. 동기 응답 (또는 무응답) 이면undefined.
sender argument는 message가 popup, content script, side panel, options page 중 어디서 왔는지 알려줘. 이 정보로 routing하고, 허용하지 않은 context의 요청을 거절할 수 있어.
return true 불변식
listener 안에서 응답 전에 async 작업 — 예: chrome.storage read — 하면 listener body에서 await가 suspend 되기 전에 동기적으로 true 반환 필수:
Chrome runtime은 listener가 stack에 있는 동안만 message channel 열어 둠. return true가 runtime 한테 "sendResponse 나중에 부를게, channel 살려 둬" 라고 말해 줘. 잊으면 async path의 sendResponse는 silent no-op — caller가 영원히 기다리다 한 5 분 뒤 timeout. MV3 message-passing의 가장 흔한 버그 한 가지.
두 방향, 두 패턴
Popup → SW. popup은 잠깐 살다 가. 그래서 SW 한테 뭘 해 달라고 부탁해 놓고 답을 기다리는 쪽이야. 흔한 부탁은 이런 것들이지 — "지금 clip 목록 줘", "이 clip 저장해 줘", "이 id 지워 줘". SW가 storage를 읽거나 쓰고 답을 돌려줘.
SW → popup. 이쪽은 흔치 않아. popup이 열려 있다는 보장이 없거든. 듣고 있는 popup이 없는데 보내면 "Receiving end does not exist" 같은 에러가 돌아와. SW가 popup에 뭔가 알려야 한다면 chrome.storage.onChanged (Lesson 4)가 훨씬 자연스러워. popup은 열릴 때 구독만 해 두고, SW는 storage에 쓰기만 하면, 양쪽이 알아서 반응하거든. "popup 열려 있나?" 를 확인하는 코드가 아예 필요 없어져.
Message와 storage 결합
등장하는 실용 ClipDeck 패턴:
- Action은 message로. "clip 저장", "clip 삭제", "전체 비우기". popup이 보내고, SW가 storage mutate, 새 state와 함께 응답.
- State update는 storage로. popup이 뜰 때 storage를 한 번 읽고, 그 뒤
onChanged를 구독. 이후 SW 쪽 mutation이 자동으로 re-render trigger.
이 분리가 codebase를 정직하게 유지: message는 동사, storage는 명사. 섞으면 — message로 전체 state 전송, message로 변경 broadcast — 작은 extension 에선 돌아가지만 clip 쌓이는 순간 spaghetti.
ClipDeck preview: popup이 SW ping
아래 exercise가 ClipDeck popup에 "Ping SW" 버튼 추가. { type: "ping" } 보내고, SW가 { ok: true, at: Date.now() } 응답, popup이 timestamp 표시. Lesson 6가 이 scaffolding을 진짜 방문 카운터 feature로 발전. Track 3 부터는 같은 channel을 ClipDeck의 실제 CRUD-C 작업 — "선택 텍스트를 clip으로 저장" 에 사용.
storage.onChanged 구독해서 무슨 일이 생겼는지 봄.sendResponse 전에 async 작업 하면 동기적으로 true 반환. 잊으면 listener return 순간 message channel 닫혀. async path의 sendResponse는 no-op. caller 영원히 대기, 결국 timeout. 증상: popup spinner가 안 풀려, 양쪽 DevTools console에 에러 안 보여.get → modify → set을 surface 둘이 동시에 돌리면 한쪽 변경이 사라질 수 있어. Service worker를 sole writer로 두고 mutation queue 하나로 직렬화하거나 record를 독립 key로 저장해. 한 번의 set은 그 안의 key를 함께 적용하지만, 앞뒤 read-modify-write 전체가 atomic인 건 아니야.Listener 호환성. Promise를 반환하는 onMessage listener는 Chrome 148부터 지원해. 더 오래된 Chrome도 받으려면 listener 자체는 async로 만들지 말고, 내부 async IIFE에서 sendResponse를 부른 다음 바깥 listener가 literal true를 반환하게 해. async listener와 return true를 섞지 마.