C.W.K.
Stream
Lesson 05 of 06 · published

Message passing — Popup 과 SW 사이의 짧고 팽팽한 줄

~12 min · messaging, runtime, sendMessage, onMessage, async, service-worker

Level 0Extension 입덕
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"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 에 message 발사. 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 list 줘", "이 clip 저장해 줘", "이 clip id 지워 줘". SW 가 storage read/write 하고 응답.

SW → popup. 덜 흔해. popup 이 열려 있다는 보장이 안 됨. 보냈는데 listening 하는 popup 없으면 SDK 가 "Receiving end does not exist" 비슷한 에러 반환. SW→popup update 는 chrome.storage.onChanged (Lesson 4) 가 자연. popup 이 열릴 때 구독, SW 가 storage write, 두 context 모두 반응. "popup 열려 있나" 가드 필요 없음.

Message 와 storage 결합

등장하는 실용 ClipDeck 패턴:

  • Action 은 message 로. "clip 저장", "clip 삭제", "전체 비우기". popup 이 보내고, SW 가 storage mutate, 새 state 와 함께 응답.
  • State update 는 storage 로. popup mount 시 storage 한 번 read, 이후 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 으로 저장" 에 사용.

Message 는 동사 (action). Storage 는 명사 (state). Message 보내서 SW 한테 뭔가 하라 고 부탁; storage.onChanged 구독해서 무슨 일이 생겼는지 봄.
Silent timeout 함정. listener 에서 sendResponse 전에 async 작업 하면 동기적으로 true 반환. 잊으면 listener return 순간 message channel 닫혀. async path 의 sendResponse 는 no-op; caller 영원히 대기, 결국 timeout. 증상: popup spinner 가 안 풀려, 양쪽 DevTools console 에 에러 안 보여.

Code

동기 round-trip — popup 보내고, SW 가 같은 tick 에 응답·javascript
// popup.js — ping 보내고 응답 await
async function pingSw() {
  const response = await chrome.runtime.sendMessage({ type: "ping" });
  console.log("[ClipDeck popup] response:", response);
  return response;
}

// background.js — 동기 응답, `return true` 안 필요
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message?.type === "ping") {
    sendResponse({ ok: true, at: Date.now() });
    return; // 동기; channel 바로 닫혀도 OK
  }
});
Async round-trip — listener 안에서 storage read/write, 동기적으로 return true·javascript
// background.js — async 응답은 `return true` 필수
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message?.type === "getClips") {
    (async () => {
      const { clips = [] } = await chrome.storage.local.get("clips");
      sendResponse({ ok: true, clips });
    })();
    return true; // async sendResponse 위해 channel 열어 둠
  }
  if (message?.type === "saveClip") {
    (async () => {
      const { clips = [] } = await chrome.storage.local.get("clips");
      const next = [...clips, message.payload];
      await chrome.storage.local.set({ clips: next });
      sendResponse({ ok: true, count: next.length });
    })();
    return true;
  }
});

External links

Exercise

clipdeck/popup.html 에 <button id="pingBtn">Ping SW</button><div id="pingResult"></div> 추가. clipdeck/popup.js 에서 버튼이 chrome.runtime.sendMessage({type:'ping'}) 호출하고 JSON-stringify 된 응답을 pingResult 에 적도록 연결. clipdeck/background.js 에 onMessage listener 등록, {type:'ping'}{ok:true, at:Date.now()} 로 동기 응답. extension reload, popup 열기, Ping SW 클릭 — div 에 timestamp 떨어져야 함. 다음 popup 닫고 약 35 초 기다려 SW 가 evict 되게 한 다음, ClipDeck toolbar icon 다시 클릭, Ping SW 클릭 — 여전히 동작해? (동작해야 함 — incoming message 가 evict 된 SW 의 wake-up trigger 중 하나.)
Hint
popup 응답이 undefined 로 돌아오면 popup DevTools console 에서 Receiving end does not exist 확인. 보통 message 도착 시 SW 에 listener 가 등록 안 된 상태 — 등록이 cold start 에서 아직 안 돈 async function 안에 묻혀 있거나, background.js 자체가 load 실패 (service-worker DevTools 에서 syntax error 점검). onMessage 등록은 background.js top-level 에서, async function 이나 다른 event handler 안에서는 절대 안 됨. 그리고 sendResponse 는 message 당 한 번만 동작; 두 번 부르면 두 번째는 silent no-op.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

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

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