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

Event-driven vs Persistent — Idle Worker

~11 min · service-worker, lifecycle, eviction, events, state-loss

Level 0Extension 입덕
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"MV3 service worker 는 user 관점에서 두 상태밖에 없어: '지금 뭔가 하는 중' 과 'idle, evict 됐을 수도'. 그 on/off 형태 위에 설계하는 게 Track 2 의 전체 mental model."

Idle clock 은 즉시 시작

Worker 의 마지막 in-flight event 가 완료되는 순간, Chrome 이 clock 을 시작. 약 30 초 무활동 — event fire 없음, async 작업 pending 없음, in-flight Promise 없음 — 후 worker evict. 메모리 해제. Global 사라짐. Module-level MapSet 증발.

Clock 은 memory pressure 또는 Chrome 내부 휴리스틱이 worker 가 안 쓸모 있다고 판단하면 더 빨리 만료. 30 초는 soft maximum 으로 다뤄, 보장 아냐.

Worker 를 깨우는 것

Worker 가 listener 등록한 어떤 event 든. 큰 것들:

  • chrome.runtime.onMessage — popup, content script, 또는 다른 extension 이 message 보냄
  • chrome.alarms.onAlarm — 예약된 alarm fire (setTimeout/setInterval 의 MV3 대체)
  • chrome.tabs.* event — tab created / updated / removed / activated / replaced
  • chrome.runtime.onInstalled / onStartup — extension install 또는 Chrome launch
  • chrome.runtime.onConnect — popup 또는 content script 에서 long-lived port 열림
  • chrome.webNavigation.* — page navigation event (webNavigation permission 필요)
  • chrome.contextMenus.onClicked — user 가 등록된 context menu entry 활성화
  • chrome.action.onClickeddefault_popup 없는 상태에서 toolbar icon 클릭

이 중 어느 게 fire 하면 Chrome 이 worker 를 cold 로 깨워: background.js 위 부분 다시 실행, listener 재등록 (이게 중요 — 등록이 Chrome 한테 dispatch 하라는 신호), 그 다음 Chrome 이 event 전달.

Eviction 때 죽는 것

Worker evict 때 잃는 거:

  • Module-level 변수 (let counter = 0, const cache = new Map() 등)
  • In-flight Promise (cancel 됨; .then chain 절대 fire 안 함)
  • WebSocket 연결, EventSource 연결, MediaStream handle, BroadcastChannel port
  • Listener 안에서 이전 wake 에 set 된 closure-captured state

살아남는 거:

  • chrome.storage.local — eviction 전에 write 됐으면 다음 wake 에 read 가능
  • chrome.storage.session — 2023 추가; worker eviction 견디지만 browser session 끝나면 죽음
  • chrome.alarms — 등록된 alarm 은 worker 재시작 너머로 영속
  • 등록된 listener 자체 (Chrome 이 기억; background.js 위 부분 재실행이 재attach 메커니즘)

Cold-wake 패턴

Cold wake = worker 가 매번 1 행에서부터 실행. background.js 를 매 event 마다 도는 main() 함수처럼 다뤄:

  1. 해당 handler 위에서 필요한 state 를 chrome.storage 에서 read.
  2. 일 하기.
  3. 새 state 를 chrome.storage 로 write back.
  4. Handler return (또는 Promise resolve).
  5. Worker idle 됨. 결국 evict. 다음 event 대기.

가장 깔끔한 mental model: 모든 event handler 가 전체 프로그램. Read / work / write / return.

피할 anti-pattern

MV2 에선 멀쩡해 보이지만 MV3 에선 능동적으로 깨지는 패턴:

  • 주기적 작업에 setInterval(fn, 60000) — timer 가 fire 하기 전에 worker evict 가능. chrome.alarms.create({periodInMinutes: 1}) 사용; alarm 은 eviction 너머로 영속하고 worker 다시 깨움.
  • Module-level cache (const userCache = new Map()). 매 wake 마다 reset. Value 에 명시적 expiry timestamp 박아서 chrome.storage.local 사용.
  • Long-lived WebSocket / EventSource hold. Worker eviction 이 죽임. 진짜 필요하면 event 마다 재수립; 또는 user 가 계속 여는 tab 의 content script 에 연결 옮김.
  • Event 사이 "session state" 로 global 쓰기. Module-level 변수는 항상 ephemeral 로 다뤄. 잠깐 살아남는 것처럼 보여도 의존하지 마.
Worker 는 프로세스가 아니라 함수. 각 event handler 가 하나의 완전한 호출. State 는 scope 가 아니라 storage 에 살아.
MV2 튜토리얼이 "background.js 의 global 에 state 박아둬" 라고 말하면 MV3 한테는 능동적으로 틀린 거. Worker 는 evict 되고 그 state 는 사라져 — 보통 최악의 타이밍에, user 가 작업 중간이고 버그 재현 불가능할 때. State 얘기에 chrome.storage 언급 없는 옛 답변은 의심해.

Code

Anti-pattern — in-memory counter 가 매 worker wake 마다 reset·javascript
// ❌ ANTI-PATTERN — module-level state dies on eviction
let messageCount = 0;

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  messageCount++;
  console.log("[ClipDeck SW] message count:", messageCount);
  sendResponse({ count: messageCount });
  return false;
});
// First few clicks: 1, 2, 3, 4...
// After ~30 seconds idle and a fresh event: 1.
// The variable is gone.
올바른 패턴 — event 마다 chrome.storage.local round-trip·javascript
// ✅ CORRECT — state lives in chrome.storage, survives eviction
chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
  const { messageCount = 0 } = await chrome.storage.local.get("messageCount");
  const newCount = messageCount + 1;
  await chrome.storage.local.set({ messageCount: newCount });
  console.log("[ClipDeck SW] message count:", newCount);
  sendResponse({ count: newCount });
  return true; // async sendResponse — keep channel open
});
// Across evictions: 1, 2, 3, 4, ... persistent.

External links

Exercise

Anti-pattern 코드 (module-level messageCount + onMessage listener) 를 background.js 에 임시로 추가. popup.js 에 chrome.runtime.sendMessage({type: 'ping'}).then((r) => console.log('SW replied:', r)) 한 줄 추가 — popup 열 때마다 message 하나 보냄. Reload. ClipDeck popup 을 연속 여러 번 열어 — counter 증가 (1, 2, 3...). 이제 chrome://serviceworker-internals 가서 ClipDeck 찾아 'Stop' 클릭 (강제 eviction). 몇 초 대기. 다시 popup 열기. 새 count 는? Reset 확인 후 listener 를 두 번째 code block 의 chrome.storage.local 패턴으로 바꾸고 실험 반복 — 이제 강제 eviction 너머로도 count 영속.
Hint
chrome://serviceworker-internals 의 강제 'Stop' 이 eviction 데모하는 가장 빠른 길; 자연 ~30 초 idle 도 되지만 더 오래 걸려. Popup 이 SW reply 기다리며 hang 하면 보통 listener 가 sendResponse 호출 없이 return 한 거 — anti-pattern listener 는 sendResponse 동기적으로 호출 후 false 반환, 올바른 패턴은 storage await 전에 true 반환 확인.

Progress

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

댓글 0

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

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