본문으로 건너뛰기
← 블로그로 돌아가기
튜토리얼 2026년 3월 5일

Chrome Extension 개발 — Manifest V3 실전 가이드

Manifest V3로 Chrome 확장 프로그램을 만드는 실전 과정. 권한, 서비스 워커, 콘텐츠 스크립트.

Chrome Extension Manifest V3 JavaScript React

Manifest V3가 달라진 점

Chrome은 2024년부터 Manifest V2 확장을 스토어에서 제거하기 시작했다. V3로 넘어오면서 바뀐 핵심은 세 가지다.

  • 백그라운드 페이지 → 서비스 워커: 상시 실행이 아니라 이벤트 기반으로 동작
  • chrome.webRequest 제한: 요청 차단이 declarativeNetRequest로 대체
  • 원격 코드 실행 금지: CDN에서 JS를 로드해 실행할 수 없음

Marginy를 만들면서 V3에 적응하는 데 가장 오래 걸린 부분은 서비스 워커의 생명주기였다.

프로젝트 구조

marginy-extension/
├── manifest.json
├── src/
│   ├── background/
│   │   └── service-worker.ts
│   ├── content/
│   │   └── inject.ts
│   ├── popup/
│   │   ├── App.tsx
│   │   └── index.html
│   └── utils/
│       └── storage.ts
├── vite.config.ts
└── package.json

Popup은 React로 만들었다. Vite의 멀티 엔트리 빌드로 popup, content script, service worker를 각각 번들링한다.

manifest.json 핵심

{
  "manifest_version": 3,
  "name": "Marginy",
  "version": "1.0.0",
  "permissions": ["storage", "activeTab"],
  "host_permissions": ["https://*.coupang.com/*", "https://*.naver.com/*"],
  "background": {
    "service_worker": "src/background/service-worker.js",
    "type": "module"
  },
  "content_scripts": [{
    "matches": ["https://*.coupang.com/*"],
    "js": ["src/content/inject.js"]
  }],
  "action": {
    "default_popup": "src/popup/index.html"
  }
}

permissionshost_permissions가 분리된 게 V3의 특징이다. 사용자에게 “모든 사이트 접근”을 요청하지 않고 필요한 도메인만 명시할 수 있다.

서비스 워커의 함정

서비스 워커는 30초간 이벤트가 없으면 종료된다. 이건 상태를 메모리에 들고 있으면 안 된다는 뜻이다.

// 잘못된 방법 — 서비스 워커가 종료되면 사라짐
let cachedData = null;

// 올바른 방법 — chrome.storage 사용
chrome.storage.local.set({ cachedData: data });
chrome.storage.local.get('cachedData', (result) => {
  // result.cachedData
});

Marginy에서는 계산 결과를 chrome.storage.local에 캐시한다. 서비스 워커가 재시작되어도 데이터가 유지된다.

콘텐츠 스크립트와 메시지 패싱

콘텐츠 스크립트는 웹페이지의 DOM에 접근할 수 있지만, Chrome API는 제한적으로만 쓸 수 있다. 서비스 워커와 통신하려면 메시지 패싱을 사용한다.

// content script → service worker
chrome.runtime.sendMessage({ type: 'CALC_PROFIT', data: productInfo });

// service worker에서 수신
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.type === 'CALC_PROFIT') {
    const result = calculateProfit(msg.data);
    sendResponse(result);
  }
  return true; // 비동기 응답을 위해 true 반환
});

return true를 빠뜨리면 비동기 응답이 전달되지 않는다. 디버깅하기 어려운 버그다.

개발/디버깅 팁

  • chrome://extensions에서 “개발자 모드” 활성화 후 “압축해제된 확장 프로그램을 로드합니다” 클릭
  • 서비스 워커 로그는 확장 프로그램 상세 페이지의 “서비스 워커” 링크에서 확인
  • 콘텐츠 스크립트 로그는 해당 웹페이지의 개발자 도구 콘솔에서 확인
  • chrome.storage.local.get(null, console.log)로 전체 저장 데이터 확인

정리

Manifest V3는 보안과 성능을 위한 변경이지만, 기존 V2 패턴에 익숙하다면 적응 기간이 필요하다. 핵심은 서비스 워커가 언제든 죽을 수 있다는 전제로 설계하는 것이다.