Manifest V3 마이그레이션 함정 — 17개 Chrome 확장 프로그램에서 얻은 교훈
Source: Dev.to
Google의 Manifest V3 마이그레이션 마감일은 이미 지나갔습니다. 17개의 Chrome 확장 프로그램을 MV2에서 MV3로 마이그레이션한 뒤, 저는 모든 함정, 우회 방법, 그리고 배운 교훈을 정리했습니다.
아직 마이그레이션 중이거나 새 확장 프로그램을 만들고 있다면, 이 가이드를 통해 몇 주간의 디버깅 시간을 절약할 수 있을 것입니다.
Source: …
1. Service‑worker lifecycle & lost global state
문제 – MV3는 지속적인 백그라운드 페이지를 서비스 워커로 대체합니다. 서비스 워커는 약 30초 동안 활동이 없으면 종료되므로, 전역 변수에 저장된 모든 상태가 사라집니다.
무엇이 깨졌는가 – 내 구독 확인 코드는 사용자의 결제 상태를 변수에 저장했습니다. 서비스 워커가 재시작된 후 해당 변수는 undefined가 되었고, 유료 사용자는 무료 티어 제한을 보게 되었습니다.
해결 방법 – 전역 변수에 상태를 저장하지 마세요. 모든 데이터를 chrome.storage에 저장하세요.
// BAD: Lost when service worker restarts
let userIsPaid = false;
// GOOD: Persisted across restarts
async function isPaid(): Promise {
const { subscriptionCache } = await chrome.storage.local.get('subscriptionCache');
return subscriptionCache?.paid ?? false;
}
추가 함정 – chrome.storage.session이 존재하지만 기본적으로 서비스 워커에서만 접근할 수 있습니다.
팝업이나 콘텐츠 스크립트에서 사용해야 한다면, 서비스 워커에서 다음을 호출하세요:
chrome.storage.session.setAccessLevel({ accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS' });
2. chrome.webRequest → declarativeNetRequest
The problem – chrome.webRequest.onBeforeRequest with blocking capability no longer exists. Extensions that modified or blocked requests must use declarativeNetRequest.
문제 – 차단 기능이 있는 chrome.webRequest.onBeforeRequest가 더 이상 존재하지 않습니다. 요청을 수정하거나 차단하던 확장 프로그램은 declarativeNetRequest를 사용해야 합니다.
What broke – FocusGuard used webRequest to redirect blocked sites. The entire blocking mechanism stopped working.
무엇이 깨졌는가 – FocusGuard는 차단된 사이트를 리다이렉트하기 위해 webRequest를 사용했습니다. 이로 인해 차단 메커니즘 전체가 작동을 멈췄습니다.
The fix – Migrate to declarativeNetRequest with dynamic rules:
해결 방법 – 동적 규칙을 사용해 declarativeNetRequest로 마이그레이션합니다:
await chrome.declarativeNetRequest.updateDynamicRules({
addRules: [{
id: 1,
priority: 1,
action: {
type: 'redirect',
redirect: { extensionPath: '/blocked.html' }
},
condition: {
urlFilter: '*://*.twitter.com/*',
resourceTypes: ['main_frame']
}
}],
removeRuleIds: [1] // optional: clean up old rules
});
Gotcha – Dynamic rules have a limit of 5,000 rules per extension. If you need to block thousands of URLs, use the rule_resources approach with static rulesets instead.
주의사항 – 동적 규칙은 확장당 5,000개로 제한됩니다. 수천 개의 URL을 차단해야 한다면 정적 규칙세트를 사용하는 rule_resources 방식을 이용하세요.
3. chrome.alarms 최소 주기
문제 – chrome.alarms.create는 프로덕션에서는 최소 1 분(개발 환경에서는 30 초) 간격을 강제합니다.
무엇이 깨졌는가 – 내 구독 새로 고침은 30 초 폴링 간격을 사용했습니다. 프로덕션에서는 조용히 60 초로 상승하여 오래된 데이터가 발생했습니다.
해결 방법 – 1 분 최소값을 고려하여 설계합니다. 1 분 미만 정밀도가 필요하면 서비스 워커 내부에서 setTimeout을 사용하세요 — 하지만 워커가 종료될 수 있다는 점을 기억하세요. 중요한 타이밍이 필요하면 1 분 단위의 granularity를 받아들입니다.
4. 서비스 워커가 잠들어 있을 때 메시징
문제 – 서비스 워커가 비활성 상태일 때, 콘텐츠 스크립트에서 chrome.runtime.sendMessage가 조용히 실패하거나 예외를 발생시킬 수 있습니다.
무엇이 깨졌는가 – 콘텐츠 스크립트가 구독 상태를 확인하기 위해 백그라운드에 호출했습니다. 서비스 워커가 잠들어 있으면 Promise가 영원히 대기합니다.
해결 방법 – 항상 타임아웃과 폴백을 추가합니다:
async function getSubscription(): Promise {
// 1️⃣ Check cache first
const cache = await chrome.storage.local.get('subscriptionCache');
if (cache.subscriptionCache?.timestamp > Date.now() - 300_000) {
return cache.subscriptionCache;
}
// 2️⃣ Ask background with timeout
return new Promise((resolve) => {
const timeout = setTimeout(() => resolve(cache.subscriptionCache || DEFAULT), 3000);
try {
chrome.runtime.sendMessage({ action: 'getSubscription' }, (res) => {
clearTimeout(timeout);
if (chrome.runtime.lastError || !res) {
resolve(cache.subscriptionCache || DEFAULT);
return;
}
resolve(res);
});
} catch {
clearTimeout(timeout);
resolve(cache.subscriptionCache || DEFAULT);
}
});
}
5. chrome.downloads.download 이제 사용자 제스처 필요
문제 – chrome.downloads.download()가 일부 상황에서 사용자 제스처를 요구합니다. 백그라운드 스크립트에서 프로그래밍 방식으로 다운로드하면 실패할 수 있습니다.
무엇이 깨졌는가 – DataPick의 내보내기 기능이 콘텐츠 스크립트에서 백그라운드로 다운로드를 트리거했습니다. MV2에서는 작동했지만 MV3에서는 조용히 실패했습니다.
해결 방법 – 다음 중 하나를 선택하십시오:
- Blob URL과 앵커 클릭을 사용하여 콘텐츠 스크립트에서 직접 다운로드를 트리거하거나, 또는
- 사용자 행동에 대한 메시지에 직접 응답하는 형태로 백그라운드 다운로드가 실행되도록 합니다.
6. chrome.tabs.executeScript → chrome.scripting.executeScript
문제 – chrome.tabs.executeScript가 chrome.scripting.executeScript로 교체되었으며 API 형태가 달라졌습니다.
이전 방식
chrome.tabs.executeScript(tabId, { code: 'document.title' });
새로운 방식
const [result] = await chrome.scripting.executeScript({
target: { tabId },
func: () => document.title,
});
console.log(result.result); // The page title
주의점 – func 매개변수는 직렬화 가능한 함수여야 합니다. 외부 스코프의 변수를 참조할 수 없습니다. 데이터를 args 매개변수를 통해 전달하세요.
7. 더 엄격해진 MV3 검토 프로세스
문제 – MV3 확장 프로그램은 더 엄격한 검토를 받습니다. Google은 이제 광범위한 권한(\<all_urls\>, tabs 등)과 큰 번들 크기를 가진 확장 프로그램에 플래그를 지정합니다.
무엇이 깨졌는가 – 두 개의 확장 프로그램이 activeTab + <all_urls>를 동시에 요청했기 때문에 중복으로 간주되어 거부되었습니다.
해결 방법
- 최소 권한만 요청합니다.
- 가능한 경우 호스트 권한 대신
activeTab을 사용합니다. - CWS 개발자 대시보드에 권한 사유를 제공하십시오.
- 번들 크기를 작게 유지합니다(적극적인 트리‑쉐이킹).
8. 마이그레이션 후 체크리스트
17개의 마이그레이션을 거친 뒤, 제가 즐겨 사용하는 체크리스트입니다:
- 모든 전역 상태를
chrome.storage로 교체 -
webRequest를declarativeNetRequest로 마이그레이션 -
chrome.tabs.executeScript를chrome.scripting.executeScript로 교체 - 모든
runtime.sendMessage호출에 타임아웃/폴백 추가 - 서비스 워커 재시작으로 테스트 (
chrome://serviceworker-internals) - 최소 1분 알람이 정상 동작하는지 확인
- 권한을 검토하고 최소화
- SW가 슬립 상태가 된 뒤 콘텐츠 스크립트 ↔ 백그라운드 통신 테스트
- 지속적인 백그라운드 없이 다운로드가 정상 동작하는지 확인
TL;DR
MV3는 근본적으로 다른 프로그래밍 모델입니다. 서비스 워커 라이프사이클이 모든 것을 바꾸어 놓습니다. 무상태성을 처음부터 설계하고, 서비스 워커를 영구적인 백그라운드 페이지가 아니라 일시적인 헬퍼로 취급하세요. 위 체크리스트를 따르면 가장 흔한 함정을 피할 수 있습니다.
Built by S‑Hub — 17 Chrome extensions, all running on Manifest V3.
- Procshot — 브라우저 단계 자동 캡처
- DataPick — 모든 웹페이지에서 데이터 추출
- FocusGuard — 산만한 사이트 차단