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

MV3 Gotchas — MV2 에서 뭐가 깨졌어

~10 min · mv3, migration, gotchas, service-worker, deprecations

Level 0Extension 입덕
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"인터넷에 떠 있는 모든 MV2-시대 튜토리얼이 지뢰밭이야. 패턴은 여전히 자명해 보여 — 그저 더 이상 load 가 안 될 뿐."

Background page → Service worker

가장 큰 단절. MV2 의 persistent background page 는 전체 browser session 동안 메모리에 살아 있었어 — global 에 state 박아두고, long-lived event listener 잡고, cache 쌓고, 다 그대로 유지. MV3 의 service worker 는 event-driven 이고 inactivity 약 30 초 후 evict 돼. Event (message / alarm / tab change / install) 로 깨우면 cold 시작 — global 초기화, in-memory cache 비움, listener 도 파일 위에서부터 재등록.

Migration 패턴: eviction 견뎌야 하는 건 chrome.storage.local 로 (synchronous 느낌, worker 재시작 across 영속). MV2 에서 in-memory state 로 들고 있던 건 storage round-trip 으로 바뀌거나, 매 wake 때 죽는 걸 받아들여.

Blocking webRequest → declarativeNetRequest

MV2 의 chrome.webRequest 는 extension 이 모든 request intercept, header 검사, 동기적으로 block 또는 modify 가능. 그게 ad blocker 의 전체 메커니즘이었어. MV3 가 비-enterprise extension 에 대해 blocking 모드 deprecate 하고 declarativeNetRequest 도입 — rule 기반 시스템, 앞으로 패턴 선언하면 Chrome 이 코드 없이 enforce.

Tradeoff: rule 기반은 더 빠르고 privacy-preserving (extension 이 자기가 안 다루는 request 자체를 못 봄), 근데 덜 유연. 복잡한 ad blocker 들은 matching logic 을 static rule 로 재설계해야 했어. ClipDeck 한테는 이 섹션 정보용 — network interception 필요 없음.

chrome.extension.* → chrome.runtime.*

여러 MV2 API 가 MV3 에서 이름 바뀌거나 chrome.runtime 으로 통합:

  • chrome.extension.getBackgroundPage() → 대체 없음 (service worker 는 "가져올" 수 없어 — chrome.runtime.sendMessage 로 메시지 보내).
  • chrome.extension.getViews() → popup/options page 용은 여전히 동작, service worker 는 "볼 수" 없음.
  • chrome.extension.sendMessagechrome.runtime.sendMessage.
  • chrome.extension.onMessagechrome.runtime.onMessage.

chrome.extension 쓰는 MV2 스니펫을 복사해 붙이면, 절반은 동작하고 절반은 안 동작. Errors panel 이 빠진 거 한 chrome.extension.X is not a function 으로 보여줘. 보통 find-and-replace 면 충분; Chrome 의 migration 문서에서 symbol 별 mapping 확인.

Persistent state 손실

모든 MV2 dev 가 딱 한 번씩 걸리는 gotcha. 코드는 멀쩡해 보이고, event listening 도 잘 되는데, state 가 랜덤하게 reset 되는 것처럼 보임. 실제로 일어난 일: service worker 가 evict 되고, 다음 event 에 재시작, 파일 위에서부터 다시 실행 — 그래서 아까 채워둔 module-level const cache = new Map() 이 새 빈 Map.

해결: service worker 를 stateless 로 취급하고 chrome.storage.local 을 state layer 로 사용. 각 event handler 위에서 read, 아래에서 write. Track 2 가 이 패턴 깊이 다룸; 지금은 gotcha 자체가 lesson.

Migration 체크리스트

혹시 MV2 extension 을 MV3 로 migrate 해야 한다면 이 list 따라가:

  1. manifest_version: 2 → 3.
  2. background.scripts + persistent: truebackground.service_worker (단일 파일).
  3. Extension HTML page 의 inline <script> 태그나 onclick= handler 다 제거.
  4. 모든 chrome.extension.* 호출 찾아서 chrome.runtime.* 로 매핑 (문서 보고).
  5. Blocking webRequestdeclarativeNetRequest rule (또는 rule 기반이 불가능하면 enterprise policy 로 escalate).
  6. 모든 module-level state 를 chrome.storage.local 로 이동.
  7. permissions 감사: permissions (API) 와 host_permissions (URL pattern) 으로 분리.
  8. web_accessible_resources 형태 확인 — MV3 가 배열을 resources / matches 가진 object 로 감싸.

이 순서가 MV2 → MV3 migration 의 약 95% 잡아. 남은 5% 는 자기만의 Chrome 문서 deep-dive 가치 있는 edge case.

MV3 는 feature 를 추가한 게 아냐 — footgun 을 제거한 거. 모든 gotcha 는 의도된 cut: 더 적은 메모리, 더 적은 attack surface, 더 적은 디버깅할 state. 제약 자체가 설계.
옛 Stack Overflow 답변이 거짓말함. MV2 패턴이 10 년간 Chrome extension 콘텐츠를 지배했어. 의심스러우면 2023 년 이전 자료보다 현재 Chrome Developers 문서를 우선. 문서 URL 에 /develop/ 들어가; MV2-시대 가이드는 /extensions/ 였어.

Code

Cross-context 호출: MV2 직접 reach vs MV3 message passing·javascript
// MV2 — gone. Service worker can't be "gotten" directly.
const bg = chrome.extension.getBackgroundPage();
bg.someSharedFunction();

// MV3 — message the worker instead.
const response = await chrome.runtime.sendMessage({
  type: "do-the-thing",
  payload: { x: 1 },
});

// And in background.js:
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === "do-the-thing") {
    sendResponse({ ok: true, result: doIt(message.payload) });
  }
  return false; // synchronous response
});
Module-level state: MV2 Set vs MV3 chrome.storage.local round-trip·javascript
// MV2 — synchronous in-memory state survives forever
let visitedTabs = new Set();
chrome.tabs.onUpdated.addListener((tabId, info, tab) => {
  if (info.status === "complete") visitedTabs.add(tab.url);
});
// later... visitedTabs.size is the running count

// MV3 — state must survive worker eviction
chrome.tabs.onUpdated.addListener(async (tabId, info, tab) => {
  if (info.status !== "complete") return;
  const { visitedUrls = [] } = await chrome.storage.local.get("visitedUrls");
  if (!visitedUrls.includes(tab.url)) {
    visitedUrls.push(tab.url);
    await chrome.storage.local.set({ visitedUrls });
  }
});

External links

Exercise

GitHub 에서 MV2-시대 Chrome extension 아무거나 찾기 (github.com 에서 "manifest_version: 2 chrome extension" 검색 — 버려진 hobby project 많아). manifest.json 과 JavaScript 파일 하나 읽기. MV3 로 migration 시 변경 필요한 MV2 idiom 셋 짚기: 어떤 manifest field, 어떤 chrome.* API 호출, 어떤 background 패턴. Migration 직접 안 해도 됨 — 뭘 바꿀지 + 왜 짧은 bullet 셋으로.
Hint
결정적 신호: manifest.json 의 background.scripts + persistent: true, JS 안의 chrome.extension.* 호출, HTML 파일의 inline event handler. Extension 이 webRequest blocking 쓰면 더 까다로운 migration (rule 기반 재설계). 빠른 win 위해 — Tampermonkey 스타일 ad-hoc script 를 extension 으로 옮긴 거 찾아봐, 거의 다 MV3 CSP 위반하는 inline JS 갖고 있어.

Progress

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

댓글 0

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

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