프롤로그 — MV3는 끝났다, 진짜로
브라우저 익스텐션 개발자들은 5년 동안 "Manifest V3는 곧 강제된다"는 말을 들었다. 매번 연기되었다. 2022년 1월. 2023년 1월. 2024년 1월. 그리고 2024년 6월, Chrome은 마침내 그 말을 지켰다.
2024년 6월부터 Chrome 안정 채널에서 MV2 익스텐션은 비활성화되었다. Chrome Web Store에서 MV2 신규 등록은 그보다 1년 전에 이미 막혔다. 2025년에는 MV2 익스텐션을 사용하던 엔터프라이즈 정책(`ExtensionManifestV2Availability`)마저 단계적으로 폐지되었고, 2026년 5월 현재 MV2는 사실상 박물관의 유물이다.
이 변화의 무게는 단순히 "manifest 버전이 올라갔다"는 게 아니다.
- **Background Page는 Service Worker가 되었다.** 영구 실행되던 페이지가 이벤트 기반으로 시들고 깨어나는 워커가 되었다.
- **blocking webRequest는 declarativeNetRequest로 대체되었다.** uBlock Origin이 Chrome에서 사라지고, uBlock Origin Lite가 등장했다.
- **호스트 권한은 런타임 옵트인이 기본이 되었다.** "모든 사이트 접근"은 더 이상 설치 시 자동 승인이 아니다.
- **Side Panel API, Offscreen Documents API, Scripting API, declarativeNetRequest** — 새로운 API들이 백그라운드 페이지 시대에는 불가능했던 일들을 가능하게 한다.
동시에 익스텐션 개발 도구도 진화했다. **Plasmo**가 React를 일급 시민으로 받아들였고, **WXT**가 Vite 기반으로 가볍고 빠른 대안을 들고 나왔다. **vite-plugin-web-extension**은 더 미니멀한 길을 제공한다. 이 글은 그 지형 전체를 본다.
1장 · 2026년 브라우저 익스텐션 — MV3 시대의 풍경
먼저 큰 그림 한 장.
Browser Extension API
|
+-------------------+-------------------+
| | |
Chrome (Blink) Firefox (Gecko) Safari (WebKit)
| | |
manifest_version: 3 manifest_version: 3 manifest_version: 3
service_worker background.scripts background.scripts
(event page) (persistent: false)
| | |
declarativeNetRequest webRequest blocking declarativeNetRequest
(MV2 호환 유지) (제한적 webRequest)
| | |
chrome.* namespace browser.* + chrome.* browser.* (Promise)
Side Panel API sidebarAction 일부 미지원
Offscreen Documents backgroundScripts 일부 미지원
위 그림 한 장이 2026년 익스텐션 개발의 본질을 요약한다.
- **Chrome (그리고 Edge·Brave·Opera 같은 Chromium 파생)**: MV3 only. blocking webRequest 없음.
- **Firefox**: MV3 지원하되, blocking webRequest를 일부 유지(특히 광고 차단 익스텐션을 위해). 백그라운드 스크립트는 event page 모델(휴면/깨움).
- **Safari**: WebKit 기반. iOS/iPadOS에서도 익스텐션 가능(Safari 15+, iOS 15+). 일부 API 미지원.
세 브라우저는 **WebExtensions** 표준(W3C Browser Extensions Community Group)을 공유하지만, 디테일에서 갈라진다. "한 번 짜서 모두 돌린다"는 슬로건은 절반만 진실이다.
익스텐션 구조의 4대 컴포넌트
┌──────────────────────────────────────────────────────┐
│ 익스텐션 패키지 (.crx / .xpi / .pkg) │
├──────────────────────────────────────────────────────┤
│ manifest.json (메타데이터 + 권한) │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Service Worker │ │ Content Script │ │
│ │ (background) │◀──▶│ (페이지 주입) │ │
│ └─────────────────┘ └─────────────────┘ │
│ ▲ ▲ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Popup / UI │ │ Side Panel │ │
│ │ (browser_action)│ │ (Chrome 114+) │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ Offscreen Documents (DOM 필요한 작업) │
└──────────────────────────────────────────────────────┘
각 컴포넌트는 별도의 실행 컨텍스트다. 통신은 메시지 패싱(`chrome.runtime.sendMessage` 등)으로 한다.
2장 · Manifest V3 — 2024년 6월 강제 적용 이후
가장 기본적인 manifest.json (Chrome MV3)
{
"manifest_version": 3,
"name": "My Extension",
"version": "1.0.0",
"description": "A simple MV3 extension",
"icons": {
"16": "icons/16.png",
"48": "icons/48.png",
"128": "icons/128.png"
},
"action": {
"default_popup": "popup.html",
"default_icon": "icons/16.png"
},
"background": {
"service_worker": "background.js",
"type": "module"
},
"content_scripts": [
{
"matches": ["https://*.example.com/*"],
"js": ["content.js"]
}
],
"permissions": ["storage", "tabs", "sidePanel"],
"host_permissions": ["https://api.example.com/*"],
"side_panel": {
"default_path": "sidepanel.html"
}
}
MV2 → MV3 차이의 핵심
| 항목 | MV2 | MV3 |
| --- | --- | --- |
| Background | persistent page / event page | Service Worker (또는 Firefox의 event page) |
| webRequest blocking | 가능 | declarativeNetRequest로 대체 (Firefox 일부 예외) |
| Host permissions | install 시 자동 승인 | 런타임 옵트인 가능(`activeTab`/`optional_host_permissions`) |
| Remote code | 허용 | 금지 (모든 JS는 패키지에 포함) |
| Content Security Policy | 객체 형태 | 더 엄격한 객체 형태 |
| Action API | `browser_action` + `page_action` | 통합된 `action` |
| Promise API | 일부만 | 모든 chrome.* API가 Promise 지원 |
`remote code` 금지는 큰 변화다. 광고 차단기들이 원격 필터 목록을 동적 평가하지 못하게 되었고, 분석/실험 도구들이 클라우드에서 JS를 받아 실행하던 패턴이 모두 사라져야 했다.
CRX 포맷 — 익스텐션은 어떻게 패키징되는가
Chrome 익스텐션은 `.crx` 파일로 배포된다. 이는 본질적으로 **헤더가 붙은 ZIP 파일**이다.
[CRX3 Magic: "Cr24"] [Version: 3] [Header Length] [Header (ProtoBuf)]
└─ RSA Signature (개발자 키)
└─ ECDSA Signature (Google 또는 자가)
[ZIP archive of extension files]
서명 키는 익스텐션의 **ID를 결정**한다. 같은 키로 서명하면 같은 ID가 나오고, 다른 키로 서명하면 다른 ID가 된다. Web Store에 등록하면 Google이 그 키를 관리한다.
3장 · Service Worker — background page를 대체
MV3의 핵심 변화는 백그라운드 컨텍스트의 모델이 바뀐 것이다.
Service Worker의 라이프사이클
[이벤트 발생] -> [Worker 깨움] -> [핸들러 실행] -> [Idle 감지]
|
v
[30초 후 종료]
Service Worker는 **휴면 가능**하다. 30초 정도 유휴 상태가 지속되면 브라우저가 종료시킨다. 다음 이벤트가 발생하면 다시 깨어난다.
이게 의미하는 바:
- **전역 변수에 상태를 저장할 수 없다.** 워커가 죽으면 사라진다. `chrome.storage.session` 또는 `chrome.storage.local`을 써야 한다.
- **timer가 살아남지 않는다.** `setTimeout`/`setInterval` 대신 `chrome.alarms`를 써야 한다.
- **WebSocket을 영구 유지할 수 없다.** 연결이 끊긴다. 필요하면 Offscreen Document에서 유지.
- **DOM이 없다.** 워커 컨텍스트는 DOM API를 못 쓴다. parse가 필요하면 Offscreen Document.
전형적인 Service Worker 패턴
// background.ts
chrome.runtime.onInstalled.addListener(() => {
console.log('Extension installed')
chrome.storage.local.set({ installedAt: Date.now() })
})
chrome.action.onClicked.addListener(async (tab) => {
if (!tab.id) return
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => alert('Hello from extension!'),
})
})
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'PING') {
sendResponse({ type: 'PONG', timestamp: Date.now() })
}
return true // 비동기 응답을 위해
})
// 주기 작업은 alarms로
chrome.alarms.create('hourly-sync', { periodInMinutes: 60 })
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'hourly-sync') {
syncData()
}
})
async function syncData() {
const res = await fetch('https://api.example.com/sync')
const data = await res.json()
await chrome.storage.local.set({ lastSync: data })
}
keepAlive 안티패턴
초기에는 워커 종료가 헷갈렸던 사람들이 `setInterval(() => fetch('/ping'), 20000)`로 워커를 강제로 살려두는 패턴을 썼다. 2026년에는 이게 명백한 안티패턴이다. Chrome은 이런 워커도 결국 종료시키고, 정책상 권장하지 않는다. 정공법은 **상태를 storage에 저장하고, 이벤트마다 깨어나서 처리**하는 것이다.
4장 · declarativeNetRequest — 광고 차단 영향
MV3에서 가장 논쟁적인 변화. blocking webRequest를 declarativeNetRequest(DNR)로 대체.
왜 바꿨나
Chrome 팀의 공식 입장: **성능과 프라이버시**. blocking webRequest는 모든 네트워크 요청을 익스텐션 워커에 라우팅해서 동기적으로 평가받았다. 이게 느렸고, 익스텐션이 모든 트래픽 내용을 보는 권한이 너무 광범위했다.
반대 진영의 입장: 그 권력을 익스텐션에서 빼앗아 **선언적 규칙**으로 좁혔다는 뜻은, 복잡한 광고 차단 로직(특히 코스메틱 필터, 동적 차단)이 약화되었다는 것이다.
DNR 기본 사용
{
"manifest_version": 3,
"name": "Simple Ad Blocker",
"version": "1.0.0",
"permissions": ["declarativeNetRequest"],
"host_permissions": ["<all_urls>"],
"declarative_net_request": {
"rule_resources": [
{
"id": "ruleset_1",
"enabled": true,
"path": "rules.json"
}
]
}
}
[
{
"id": 1,
"priority": 1,
"action": { "type": "block" },
"condition": {
"urlFilter": "||doubleclick.net^",
"resourceTypes": ["script", "image", "sub_frame"]
}
},
{
"id": 2,
"priority": 1,
"action": { "type": "redirect", "redirect": { "url": "https://example.com/blocked" } },
"condition": {
"urlFilter": "||tracker.com^",
"resourceTypes": ["main_frame"]
}
}
]
규칙은 **선언적**이다. 익스텐션이 매 요청마다 결정을 내리지 않고, 브라우저가 규칙 집합을 보고 결정한다.
제약사항
- **정적 규칙 제한**: 단일 익스텐션 30,000개. 모든 활성화된 익스텐션 총합 330,000개. uBlock Origin이 사용하던 100만+ 규칙 EasyList를 다 못 담는다.
- **동적 규칙**: 5,000개. 사용자가 직접 추가하는 규칙.
- **세션 규칙**: 5,000개. 브라우저 종료 시 사라짐.
- **정규식 규칙**: 매칭 시간 제한이 엄격.
uBlock Origin의 저자 Raymond Hill은 Chrome에서는 **uBlock Origin Lite**라는 MV3 호환 버전을 별도로 운영하지만, "MV2의 uBO만큼은 못 한다"고 명시적으로 적어두었다. Firefox에서는 여전히 풀 기능의 uBO가 동작한다(Mozilla가 blocking webRequest를 유지하기로 결정).
5장 · Side Panel API (Chrome 114+)
2023년 6월 Chrome 114에서 도입된 Side Panel API는 익스텐션 UX를 근본적으로 바꿨다. 팝업이 클릭하면 닫히는 휘발성 UI라면, Side Panel은 **브라우저 옆에 영속하는 UI**다.
기본 사용
{
"manifest_version": 3,
"name": "Side Panel Demo",
"version": "1.0.0",
"permissions": ["sidePanel"],
"side_panel": {
"default_path": "sidepanel.html"
},
"action": {
"default_title": "Open Side Panel"
}
}
// background.ts
chrome.runtime.onInstalled.addListener(() => {
chrome.sidePanel
.setPanelBehavior({ openPanelOnActionClick: true })
.catch((error) => console.error(error))
})
// 특정 탭에만 사이드 패널 활성화
chrome.tabs.onUpdated.addListener(async (tabId, info, tab) => {
if (!tab.url) return
const url = new URL(tab.url)
if (url.origin === 'https://www.example.com') {
await chrome.sidePanel.setOptions({
tabId,
path: 'sidepanel-example.html',
enabled: true,
})
} else {
await chrome.sidePanel.setOptions({
tabId,
enabled: false,
})
}
})
Side Panel의 강점
- **영속성**: 탭을 전환해도 같은 사이드 패널이 유지된다(혹은 탭별로 다르게).
- **공간**: 팝업은 800x600 정도가 한계지만, 사이드 패널은 더 넓다.
- **상호작용**: 사용자가 작업을 이어가는 동안 패널이 함께 보인다.
- **AI 어시스턴트와 궁합**: 채팅 UI가 사이드 패널에 자연스럽게 들어맞는다.
Edge도 사이드 패널을 지원한다(Edge의 Copilot이 정확히 이 구조). Firefox는 `sidebarAction`이라는 이름의 비슷한 API를 MV2 시절부터 가지고 있었고, MV3에서도 유지된다. Safari는 일부 버전에서 지원.
6장 · Offscreen Documents + Scripting API
Offscreen Documents — DOM이 필요할 때
Service Worker에는 DOM이 없다. 하지만 익스텐션은 가끔 DOM이 정말로 필요하다 — HTML 파싱, 클립보드 접근, `<audio>` 재생, `DOMParser` 사용, WebRTC 등.
해결책: **Offscreen Document**. 사용자에게 보이지 않는 HTML 페이지를 백그라운드에서 띄울 수 있다.
{
"permissions": ["offscreen"]
}
// background.ts
async function ensureOffscreenDocument() {
const existing = await chrome.runtime.getContexts({
contextTypes: ['OFFSCREEN_DOCUMENT'],
documentUrls: [chrome.runtime.getURL('offscreen.html')],
})
if (existing.length > 0) return
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['DOM_PARSER'],
justification: 'Parse HTML for content extraction',
})
}
chrome.action.onClicked.addListener(async () => {
await ensureOffscreenDocument()
const response = await chrome.runtime.sendMessage({
target: 'offscreen',
type: 'PARSE_HTML',
data: '<html>...</html>',
})
console.log('parsed:', response)
})
<!-- offscreen.html -->
<!doctype html>
// offscreen.ts
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.target !== 'offscreen') return
if (message.type === 'PARSE_HTML') {
const parser = new DOMParser()
const doc = parser.parseFromString(message.data, 'text/html')
const title = doc.querySelector('title')?.textContent ?? ''
sendResponse({ title })
}
})
`reasons`는 정해진 enum에서 골라야 한다: `AUDIO_PLAYBACK`, `CLIPBOARD`, `DOM_PARSER`, `DOM_SCRAPING`, `IFRAME_SCRIPTING`, `LOCAL_STORAGE`, `TESTING`, `USER_MEDIA`, `WEB_RTC`, `BLOBS`, `WORKERS`.
Scripting API — 콘텐츠 스크립트의 미래
MV2의 `chrome.tabs.executeScript`는 MV3의 `chrome.scripting.executeScript`로 대체되었다.
// 함수 주입
await chrome.scripting.executeScript({
target: { tabId: 123 },
func: (color) => {
document.body.style.backgroundColor = color
},
args: ['lightblue'],
})
// 파일 주입
await chrome.scripting.executeScript({
target: { tabId: 123, allFrames: true },
files: ['content.js'],
})
// CSS 삽입
await chrome.scripting.insertCSS({
target: { tabId: 123 },
css: 'body { filter: invert(1); }',
})
// 콘텐츠 스크립트 동적 등록
await chrome.scripting.registerContentScripts([
{
id: 'dynamic-script',
matches: ['https://*.example.com/*'],
js: ['dynamic.js'],
runAt: 'document_idle',
},
])
`scripting` 권한 + `host_permissions`(또는 `activeTab`) 조합이 필요하다.
7장 · Plasmo — React 기반 프레임워크
전통적인 익스텐션 개발은 manifest 손으로 쓰고, webpack/rollup 설정하고, 핫리로드 직접 구성하고… 지루했다. Plasmo는 이 모든 것을 **Next.js처럼** 풀어낸다.
30초 세팅
pnpm create plasmo my-extension
cd my-extension
pnpm dev
my-extension/
├── popup.tsx # 자동으로 popup이 됨
├── options.tsx # 자동으로 options page
├── background.ts # 자동으로 service worker
├── contents/
│ └── plasmo.ts # content script
├── sidepanel.tsx # side panel
└── package.json # manifest의 일부가 됨
manifest.json을 직접 작성할 필요가 없다. 파일명과 디렉터리 컨벤션으로 결정된다. package.json에 메타데이터를 적는다.
{
"name": "my-extension",
"displayName": "My Extension",
"version": "1.0.0",
"description": "...",
"manifest": {
"permissions": ["storage", "tabs"],
"host_permissions": ["https://*.example.com/*"]
}
}
Plasmo의 강점
- **React 일급**: `popup.tsx`가 즉시 React 컴포넌트.
- **HMR**: 익스텐션 코드 수정 시 자동 리로드.
- **Content Script UI**: `contents/`에 React 컴포넌트를 두면 페이지에 주입되는 UI를 React로 작성 가능.
- **Storage Hook**: `@plasmohq/storage`로 `useStorage` 훅 사용.
- **Messaging**: 타입 안전한 메시징.
- **Cross-browser**: Chrome / Firefox / Edge 빌드 분리.
Content Script UI 예시
// contents/inline.tsx
export const config: PlasmoCSConfig = {
matches: ['https://*.example.com/*'],
}
export default function ContentUI() {
const [count, setCount] = useState(0)
return (
)
}
이게 자동으로 페이지에 주입되어 동작한다. Shadow DOM 격리도 옵션으로 가능.
Plasmo의 약점
- 추상화가 두꺼워서 manifest를 미세 제어하기 어려울 때가 있다.
- 빌드 결과물이 무거워지는 경향(React 런타임 포함).
- 한때 활발하던 BrowserStack(모기업) 지원이 다소 줄었다는 평. 그래도 OSS 커뮤니티는 살아있다.
8장 · WXT — Vite 기반 신예
Plasmo가 "React-Next.js" 비유라면 **WXT**는 "Vite-Nuxt" 비유다. 더 가볍고, 더 빠르고, 더 자유롭다. 2023년 등장 이후 2025-2026년에 급격히 성장.
기본 구조
pnpm dlx wxt@latest init my-extension
cd my-extension
pnpm dev
my-extension/
├── entrypoints/
│ ├── background.ts
│ ├── content.ts
│ ├── popup/
│ │ └── index.html
│ └── options/
├── wxt.config.ts
└── package.json
// wxt.config.ts
export default defineConfig({
manifest: {
permissions: ['storage', 'tabs'],
host_permissions: ['https://*.example.com/*'],
},
modules: ['@wxt-dev/module-react'],
})
// entrypoints/background.ts
export default defineBackground(() => {
console.log('Hello from background!')
browser.runtime.onMessage.addListener((msg) => {
return Promise.resolve({ pong: true })
})
})
WXT의 강점
- **Vite 기반**: 빠른 빌드, ESM 친화.
- **프레임워크 비종속**: React 모듈도 있고, Vue 모듈도 있고, Svelte도 가능.
- **자동 import**: Nuxt 스타일의 자동 imports — `browser`, `defineBackground`, `defineContentScript` 같은 글로벌이 자동으로 들어온다.
- **Cross-browser**: 한 번 짜서 Chrome/Firefox/Safari 빌드 분리.
- **Web Ext 통합**: 개발 모드에서 브라우저를 자동 실행하고 핫리로드.
WXT vs Plasmo 한 줄 비교
| 항목 | Plasmo | WXT |
| --- | --- | --- |
| 기반 | parcel/Next.js 스타일 | Vite/Nuxt 스타일 |
| 프레임워크 | React 중심(Vue·Svelte도 가능) | 다 평등(모듈) |
| Manifest 제어 | 컨벤션 우선 | 직접 작성 가능 |
| 학습 곡선 | 낮음(컨벤션 따라가면 됨) | 중간(설정 명시적) |
| 커뮤니티 | 2022-2024 활발 → 정체 | 2024-2026 급성장 |
| 추천 시나리오 | React 위주의 빠른 프로토타입 | 장기 운영·다중 프레임워크·정밀 제어 |
2026년 기준 새로 시작한다면 **WXT가 우세**라는 게 다수 의견. Plasmo는 여전히 좋지만, 모멘텀이 WXT로 옮겨갔다.
9장 · vite-plugin-web-extension — 미니멀 옵션
프레임워크의 무게가 부담스럽다면, **vite-plugin-web-extension**이 답일 수 있다. Vite 플러그인 하나로 익스텐션 빌드를 해결.
pnpm add -D vite vite-plugin-web-extension
// vite.config.ts
export default defineConfig({
plugins: [
webExtension({
manifest: path.resolve('manifest.json'),
additionalInputs: {
html: ['sidepanel.html'],
},
}),
],
})
`manifest.json`을 직접 쓰고, Vite가 entry들을 자동 발견해서 빌드한다. 컨벤션 자동화는 없지만, **Vite의 모든 것을 그대로 쓸 수 있다.**
선택 기준
- "프레임워크 의존성 없이 가볍게" → vite-plugin-web-extension
- "Vite 생태계 + 자동화도 어느 정도" → WXT
- "React 중심으로 빠르게" → Plasmo
- "프레임워크 안 쓰고 vanilla로" → webpack 직접 / Rollup 직접 (지금은 권장 안 함)
10장 · Firefox MV3 — 일부 MV2 유지
Mozilla는 Chrome의 MV3 전환에 미묘한 반대 입장이었다. 결과적으로 Firefox MV3는 Chrome MV3와 닮았지만 다른 부분이 있다.
Firefox MV3의 차별점
- **Background**: Service Worker 대신 **event page** 모델. 휴면 가능하지만 DOM을 가질 수 있다.
- **blocking webRequest**: 유지. uBlock Origin이 Firefox에서 풀 기능으로 동작하는 이유.
- **Host permissions**: Chrome처럼 런타임 옵트인 가능.
- **API 네임스페이스**: `browser.*` (Promise 기반)이 표준. `chrome.*`도 호환을 위해 동작.
같은 코드를 양쪽에서 돌리기
// 공통 헬퍼
const api = (typeof browser !== 'undefined' ? browser : chrome) as typeof browser
async function getCookie(url: string, name: string) {
return api.cookies.get({ url, name })
}
`webextension-polyfill` 라이브러리를 쓰면 `chrome.*`도 Promise를 반환하게 만들 수 있다.
await browser.storage.local.set({ key: 'value' })
Plasmo와 WXT는 이미 이 polyfill 또는 동등한 추상을 내장하고 있다.
manifest 차이
Firefox는 `manifest.json`에 `browser_specific_settings`를 요구할 수 있다.
{
"manifest_version": 3,
"name": "Cross-Browser Ext",
"version": "1.0.0",
"background": {
"scripts": ["background.js"],
"type": "module"
},
"browser_specific_settings": {
"gecko": {
"id": "myext@example.com",
"strict_min_version": "115.0"
}
}
}
WXT는 이런 분기를 자동으로 처리해준다.
11장 · Safari Web Extensions — Mac/iOS
Apple은 2020년 Safari 14에서 Web Extensions를 받아들였다. 2021년 iOS 15부터 **iPhone/iPad에서도** 익스텐션이 가능해졌다. 이건 작은 사건이 아니다 — 모바일 익스텐션 생태계의 사실상 첫 사례다.
Safari Web Extension의 특징
- **Xcode 프로젝트로 래핑**: 웹 익스텐션 코드를 Xcode 앱 컨테이너에 넣어서 빌드한다. App Store를 통해 배포.
- **`browser.*` API 사용**: Mozilla 호환 네임스페이스.
- **일부 API 미지원**: `chrome.identity`, `chrome.sidePanel`, `chrome.declarativeNetRequest`의 일부 기능, `chrome.offscreen` 등 미지원/제한.
- **iOS 제약**: 모바일에서 권한 모델이 더 엄격. 사용자 명시 활성화 필요.
변환 도구 — safari-web-extension-converter
이미 만든 Chrome/Firefox 익스텐션을 Safari로 옮길 때:
xcrun safari-web-extension-converter /path/to/extension
이 명령이 Xcode 프로젝트 스캐폴드를 만들어준다. manifest를 읽고, 호환되지 않는 API들을 경고로 알려준다.
배포
- **macOS**: Safari Extensions Gallery는 2022년 폐쇄. 이제 모든 Safari 익스텐션은 **App Store**를 통해 배포.
- **iOS**: 동일하게 App Store.
- **Apple Developer Program** 멤버십 필요 (연 $99).
이 진입장벽이 Safari 익스텐션 생태계가 Chrome만큼 풍성하지 않은 이유다. 하지만 iOS에서 익스텐션이 가능한 유일한 길이라 점점 중요해진다.
12장 · Edge Add-ons — Microsoft
Edge는 Chromium 기반이라 Chrome 익스텐션이 거의 그대로 동작한다. 하지만 Microsoft는 **Edge Add-ons**라는 자체 스토어를 운영한다.
Edge에 익스텐션 올리기
1. Microsoft Partner Center에 개발자 계정 등록 (무료).
2. 같은 .zip 패키지를 업로드.
3. 검수.
Chrome 익스텐션과의 호환성
- **거의 100%**: Chrome MV3 익스텐션은 Edge에서 거의 그대로 동작.
- **Edge 특화 API**: 일부 `chrome.*` 대신 `edge.*`로 노출되는 API가 있지만, 대부분 동일.
- **사이드바**: Edge는 Sidebar API를 적극 활용 (Copilot 등).
Edge에서만 가능한 것
- **자동 설치 정책**: 엔터프라이즈 환경에서 IT 관리자가 강제 배포 가능.
- **Bing Chat / Copilot 통합**: 마이크로소프트 서비스와의 통합 API.
- **Edge Workspaces**: 작업 공간에 묶인 익스텐션.
Edge 사용자가 전 세계 5-7% 수준이라 작아 보이지만, **엔터프라이즈 환경에서는 Chrome 못지않게 중요**하다. B2B 익스텐션이라면 Edge 스토어 등록이 필수.
13장 · AI in 익스텐션 — Side Panel + LLM
2026년 익스텐션 개발의 가장 흥미로운 흐름은 **AI 통합**이다. ChatGPT 브라우저 익스텐션, Perplexity, Glasp, Compose AI, Wisedocs 같은 도구들이 모두 같은 패턴을 쓴다.
전형적인 AI 익스텐션 아키텍처
[Web Page]
| (사용자 액션: 텍스트 선택, 페이지 요약 요청)
v
[Content Script] -- 페이지 컨텍스트 추출 -->
|
v
[Service Worker] -- LLM API 호출 (Claude / OpenAI / 자체) -->
|
v
[Side Panel] -- 결과 스트리밍 표시 -->
|
v
[사용자]
Side Panel + 스트리밍 UI
// sidepanel/App.tsx (WXT + React)
export default function SidePanelApp() {
const [messages, setMessages] = useState<{ role: 'user' | 'assistant'; content: string }[]>([])
const [input, setInput] = useState('')
async function send() {
const userMsg = { role: 'user' as const, content: input }
setMessages((m) => [...m, userMsg])
setInput('')
const response = await chrome.runtime.sendMessage({
type: 'ASK_LLM',
messages: [...messages, userMsg],
})
setMessages((m) => [...m, { role: 'assistant', content: response.text }])
}
return (
{messages.map((m, i) => (
{m.content}
))}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && send()}
className="flex-1 border p-2"
/>
Send
)
}
// background.ts
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'ASK_LLM') {
callLLM(msg.messages).then((text) => sendResponse({ text }))
return true // async
}
})
async function callLLM(messages: any[]) {
const apiKey = (await chrome.storage.local.get('apiKey')).apiKey
const res = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
'content-type': 'application/json',
},
body: JSON.stringify({
model: 'claude-opus-4-7',
max_tokens: 1024,
messages,
}),
})
const data = await res.json()
return data.content[0].text
}
페이지 컨텍스트 활용
// content.ts
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'GET_PAGE_TEXT') {
sendResponse({
title: document.title,
url: location.href,
text: document.body.innerText.slice(0, 50000),
})
}
})
// sidepanel: 현재 탭의 텍스트 가져와서 요약 요청
async function summarizeCurrentPage() {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
if (!tab.id) return
const pageData = await chrome.tabs.sendMessage(tab.id, { type: 'GET_PAGE_TEXT' })
// ... LLM 호출
}
Chrome Built-in AI (Gemini Nano)
2024년부터 Chrome은 **On-device Gemini Nano**를 익스텐션에서 사용할 수 있게 제공하기 시작했다(Origin Trial → 정식). `chrome.ai`라는 namespace가 점진적으로 풀려나가는 중이고, 2026년 5월 기준 일부 API(prompt, summarize, translate)가 안정 채널.
// 가용성 확인
const availability = await ai.languageModel.availability()
if (availability === 'available') {
const session = await ai.languageModel.create()
const response = await session.prompt('Summarize: ...')
}
장점: 로컬 실행, 무료, 프라이버시. 단점: 모델이 작아서 정교한 작업은 한계.
비용 모델
AI 익스텐션의 흔한 비즈니스 모델 4가지.
1. **BYOK (Bring Your Own Key)**: 사용자가 자기 API 키를 넣음. 개발자는 트래픽 비용 안 듦.
2. **무료 + 한도 + 유료**: 일정 횟수 무료, 이후 구독.
3. **순수 SaaS**: 익스텐션은 클라이언트, 인증/결제는 자체 백엔드.
4. **온디바이스**: Chrome Built-in AI 또는 자체 양자화 모델로 비용 0.
14장 · 한국 / 일본 — 카카오, 네이버, ピクシブ
한국
- **카카오 익스텐션**: 카카오톡 셰어, 다음 검색, 카카오뱅크 인증 등 카카오 생태계와 연동되는 유틸 익스텐션들이 많다. 카카오는 별도의 익스텐션 SDK를 공식 제공하진 않지만, 카카오 API와 OAuth를 익스텐션에서 사용하는 사례가 풍부하다.
- **네이버 익스텐션**: 네이버 검색 결과 강화, 네이버 클라우드, 네이버 사전, 네이버 쇼핑 가격 비교(시그널, 다나와 같은 비교쇼핑 익스텐션)가 대표적.
- **토스 / 당근**: 결제·중고거래 영역에서 사내용 익스텐션을 운영하는 사례가 많다.
- **번역기**: 파파고, 네이버 번역의 익스텐션. 모바일 강세 한국에서 데스크톱 번역은 여전히 유효한 시장.
- **광고 차단**: AdBlock Plus의 한국어 EasyList Korea가 활발.
일본
- **ピクシブ 익스텐션**: pixiv 작품 다운로더, 태그 강화기, 일러스트 보기 보조 도구들이 비공식 OSS로 다수. 단, pixiv ToS와 항상 충돌 가능성이 있어 신중해야 한다.
- **Niconico 익스텐션**: 영상 다운로드 보조, 댓글 강화, 코메드 보기 개선 등.
- **Mercari / Rakuma**: 중고 마켓플레이스 가격 추적 익스텐션.
- **Rakuten 익스텐션**: 적립 포인트 자동 계산, 쿠폰 자동 적용.
- **일본어 학습**: Rikaikun, 10ten Japanese Reader, Yomichan(현재 Yomitan으로 포크) — 일본어 학습자들에게는 사실상 필수.
- **AdGuard**: 일본어 EasyList Japan 유지 활발.
카카오/네이버 OAuth in 익스텐션 예시
// chrome.identity.launchWebAuthFlow를 통한 OAuth
async function loginWithKakao() {
const clientId = 'YOUR_KAKAO_REST_API_KEY'
const redirectUri = chrome.identity.getRedirectURL()
const authUrl =
`https://kauth.kakao.com/oauth/authorize?` +
`client_id=${clientId}&` +
`redirect_uri=${encodeURIComponent(redirectUri)}&` +
`response_type=code`
const responseUrl = await chrome.identity.launchWebAuthFlow({
url: authUrl,
interactive: true,
})
const code = new URL(responseUrl!).searchParams.get('code')
// code를 백엔드로 보내서 토큰 교환
return code
}
`chrome.identity.getRedirectURL()`이 반환하는 URL을 카카오 개발자 콘솔의 Redirect URI에 등록해야 한다. 형식은 `https://<extension-id>.chromiumapp.org/`다.
한국·일본 익스텐션 개발의 현실
- 결제 모듈(카카오페이, 네이버페이, PayPay)을 직접 익스텐션에서 호출하는 건 거의 불가능. 백엔드 경유 필수.
- 인앱브라우저(카카오톡, 라인 내부 브라우저)는 익스텐션을 지원하지 않는다. 모바일에서 익스텐션을 노린다면 Safari Web Extension on iOS 정도.
- 일본은 보수적인 시장이라 익스텐션 채택률이 한국·미국보다 낮은 경향. 대신 한 번 잡으면 오래 쓴다.
15장 · 누가 무엇을 골라야 하나 — 1인 / 팀 / 크로스 브라우저 / AI 통합
1인 개발자 + 빠른 프로토타입
- **추천 스택**: Plasmo 또는 WXT.
- **이유**: HMR, 컨벤션, 보일러플레이트 최소화. 일주일 안에 사용 가능한 익스텐션 만들 수 있다.
- **타깃 브라우저**: Chrome 먼저, 잘 되면 Firefox/Edge로 확장.
- **유의**: Web Store 검수 시간(평균 1-7일)을 고려.
소규모 팀 + 장기 프로덕트
- **추천 스택**: WXT.
- **이유**: Vite 기반이라 빌드 빠르고, TS 친화적이고, 멀티 브라우저 지원이 자연스러움.
- **권고**: storybook이나 chromatic 같은 도구로 UI 회귀 테스트. e2e는 Playwright의 익스텐션 모드(BrowserContext에 익스텐션 로딩).
크로스 브라우저 우선
- **추천 스택**: WXT + `webextension-polyfill`.
- **빌드 매트릭스**: Chrome MV3, Firefox MV3, Edge MV3, Safari(별도 Xcode 프로젝트).
- **유의**: declarativeNetRequest 사용 시 Firefox 폴백을 webRequest로 분기 필요할 수 있음.
AI 통합 익스텐션
- **UI**: Side Panel 우선. 팝업은 짧은 액션용으로 한정.
- **LLM**: BYOK 모델로 시작 → 사용자 수 늘면 자체 백엔드 검토.
- **온디바이스**: Chrome Built-in AI(Gemini Nano)도 옵션. 무료라는 점이 강력.
- **컨텍스트 추출**: content script로 페이지 텍스트 + 메타데이터 추출 → Service Worker에서 LLM 호출 → Side Panel에서 스트리밍 표시.
광고 차단 / 콘텐츠 필터링
- **MV3 Chrome**: declarativeNetRequest 한도 안에서 가능. 풀 기능은 어려움.
- **Firefox**: blocking webRequest로 풀 기능 가능. uBlock Origin이 여전히 Firefox에서 살아있는 이유.
- **추천**: Firefox를 메인 타깃, Chrome은 Lite 버전으로 분기.
엔터프라이즈 / B2B
- **Chrome**: 엔터프라이즈 정책으로 강제 배포 가능(`force_install`).
- **Edge**: 같은 방식. Microsoft 365 환경에서 자연스러움.
- **권고**: 회사 도메인의 SAML SSO 연동 가능한 백엔드 구축. 익스텐션은 클라이언트 역할.
16장 · 익스텐션 보안 — 무엇을 신경 써야 하나
호스트 권한은 최소로
{
"permissions": ["activeTab"],
"optional_host_permissions": ["https://*.example.com/*"]
}
`<all_urls>`는 검수에서 strict하게 본다. `activeTab`을 우선 사용하고, 광범위한 권한이 정말 필요하면 `optional_host_permissions`로 런타임 요청.
CSP 엄격하게
{
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
}
}
`unsafe-eval`, `unsafe-inline`은 MV3에서 금지. 외부 CDN script도 금지(모든 JS는 패키지에 포함되어야 함).
externally_connectable
웹 페이지가 익스텐션에 메시지를 보낼 수 있게 하려면:
{
"externally_connectable": {
"matches": ["https://yoursite.com/*"]
}
}
이를 통해 자사 사이트가 익스텐션과 통신하는 패턴이 가능하지만, 도메인을 좁게 잡지 않으면 보안 구멍이 된다.
XSS 방어
- content script에서 페이지 DOM에 무언가 주입할 때는 `textContent` 사용, `innerHTML` 피하기.
- React를 쓰면 기본적으로 안전하지만, `dangerouslySetInnerHTML`은 신중하게.
- Trusted Types 적용 고려.
개인정보
- API 키는 `chrome.storage.local`에 저장하되, 가능하면 BYOK이거나 백엔드를 통한 토큰 교환.
- 페이지 콘텐츠를 외부로 전송할 때는 사용자에게 명시적으로 알리고 옵트인.
- Chrome Web Store의 데이터 사용 공개 정책을 반드시 따를 것 — 위반 시 익스텐션이 차단된다.
17장 · Chrome Web Store 정책 — 통과시키는 법
자주 거절되는 사유
1. **권한 과도**: `<all_urls>` 또는 광범위한 host_permissions의 정당성 부족.
2. **단일 목적 위반**: 한 익스텐션은 명확한 단일 목적. "여러 유틸을 하나로 묶음"은 거절 사유.
3. **콘텐츠 정책**: 광고 삽입, 검색 결과 조작, 어필리에이트 가로채기는 strict 금지.
4. **사용자 데이터 정책**: 데이터 수집 시 privacy policy URL 필수, 명시적 공개.
5. **소스 코드 난독화**: 의도적 난독화 금지. minify는 OK이지만 readable해야 함.
6. **결제 회피**: Chrome Web Store Payments를 안 쓰고 외부 결제로 우회하는 것 제한.
검수 시간
- **신규 등록**: 평균 1-7일, 길면 2-3주.
- **업데이트**: 보통 1-2일.
- **민감 권한(`<all_urls>`, `tabs`, `downloads`)이 있으면 더 길어짐**.
베타 채널 활용
`unlisted` 또는 `private` 배포로 베타 테스터에게 먼저 배포한 후 public 전환. 검수 한 번 더 받음.
사용자 데이터 공개 (Privacy Practices)
Chrome Web Store는 다음을 명시적으로 공개해야 한다:
- 어떤 사용자 데이터를 다루는가
- 왜 다루는가
- 어떻게 보호하는가
- 어디로 전송하는가
거짓 공개는 즉시 차단 사유.
18장 · 테스트와 CI/CD
단위 테스트
// Vitest로 헬퍼 함수 테스트
global.chrome = {
storage: {
local: {
set: vi.fn(),
get: vi.fn().mockResolvedValue({}),
},
},
alarms: {
create: vi.fn(),
onAlarm: { addListener: vi.fn() },
},
} as any
describe('syncData', () => {
it('saves data to storage', async () => {
await syncData()
expect(chrome.storage.local.set).toHaveBeenCalled()
})
})
E2E 테스트 — Playwright with Extension
test('extension loads', async () => {
const pathToExtension = path.resolve(__dirname, '../.output/chrome-mv3')
const context = await chromium.launchPersistentContext('', {
headless: false,
args: [
`--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`,
],
})
const page = await context.newPage()
await page.goto('https://example.com')
// content script가 주입되었는지 확인
const injected = await page.evaluate(() => (window as any).__MY_EXT_LOADED__)
expect(injected).toBe(true)
await context.close()
})
CI/CD
.github/workflows/release.yml
name: Build and Release
on:
push:
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install
- run: pnpm test
- run: pnpm build --target chrome-mv3
- run: pnpm build --target firefox-mv3
- uses: actions/upload-artifact@v4
with:
name: extensions
path: .output/
publish-chrome:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
- name: Upload to Chrome Web Store
uses: PlasmoHQ/bpp@v3
with:
keys: $TEST_KEYS_PLACEHOLDER
chrome-file: extensions/chrome-mv3.zip
`PlasmoHQ/bpp`(Browser Plugin Publisher) 액션이 Chrome/Firefox/Edge 자동 업로드를 지원.
19장 · 흔한 함정 모음
"Service Worker가 죽어서 작업이 끊긴다"
- 원인: 30초 유휴 후 종료.
- 해결: 상태를 `chrome.storage`에 저장. 긴 작업은 `chrome.alarms` 또는 `chrome.notifications`로 진행 상황 표시.
"WebSocket이 끊긴다"
- 원인: Service Worker 종료.
- 해결: Offscreen Document에 WebSocket을 두고 워커와 메시지 패싱.
"content script에서 페이지의 변수에 접근이 안 된다"
- 원인: Content Script는 **격리된 컨텍스트(isolated world)**에서 실행. 페이지의 `window` 변수는 안 보인다.
- 해결: `chrome.scripting.executeScript`로 `world: 'MAIN'` 옵션 사용, 또는 `<script>` 태그 주입.
"Chrome에서 잘 되는데 Firefox에서 안 된다"
- 원인: API 차이(특히 declarativeNetRequest 제한, host permission 모델).
- 해결: WXT/Plasmo의 cross-browser 빌드, `webextension-polyfill`, 분기 코드.
"검수에서 거절됐다 — 권한이 과도하다"
- 원인: `<all_urls>` 또는 광범위 host permissions.
- 해결: `activeTab` 우선, 정말 필요한 도메인만 명시.
"익스텐션이 갑자기 비활성화됐다 — 정책 위반"
- 원인: 데이터 사용 공개 누락, 단일 목적 위반, 등.
- 해결: Chrome Web Store 개발자 대시보드에서 위반 사유 확인하고 수정 후 재제출.
20장 · 에필로그 — 익스텐션은 죽었나, 살아있나
2020년대 초반에는 "PWA가 익스텐션을 대체할 것"이라는 말이 돌았다. 2026년 현재 그 예측은 절반만 맞았다. PWA가 익스텐션의 일부 영역(설치형 웹앱)을 가져갔지만, **브라우저의 모든 페이지와 상호작용하는 능력**은 익스텐션만이 가진다. 그래서 익스텐션은 죽지 않는다.
오히려 MV3의 강제 적용과 AI의 부상이 **익스텐션의 르네상스**를 가져왔다. ChatGPT, Claude, Perplexity 익스텐션은 수백만 사용자를 모았고, 매일 새로운 AI 보조 익스텐션이 출시된다. Side Panel API는 채팅 UI를 위한 캔버스가 되었고, 페이지 컨텍스트를 자유롭게 LLM에 보낼 수 있다는 점은 익스텐션만의 강점이다.
2026년 익스텐션 개발의 권고를 한 줄로 요약하면:
> **WXT 또는 Plasmo로 시작하라. Manifest V3, Service Worker, Side Panel API를 익혀라. Chrome 먼저, Firefox/Edge는 자동 빌드로, Safari는 시장이 있다면 별도 프로젝트로. AI 통합이라면 Side Panel + LLM API로 시작. 권한은 최소로, 검수 정책은 정확히, 사용자 데이터 공개는 솔직하게.**
이게 2026년 5월 시점의 정답이다. 1년 후엔 또 바뀔 것이다 — 그게 익스텐션 세계다.
참고 / References
- Chrome Extensions Manifest V3 — [https://developer.chrome.com/docs/extensions/mv3/intro/](https://developer.chrome.com/docs/extensions/mv3/intro/)
- MV2 to MV3 Migration — [https://developer.chrome.com/docs/extensions/migrating/](https://developer.chrome.com/docs/extensions/migrating/)
- Service Workers in Extensions — [https://developer.chrome.com/docs/extensions/develop/concepts/service-workers](https://developer.chrome.com/docs/extensions/develop/concepts/service-workers)
- declarativeNetRequest API — [https://developer.chrome.com/docs/extensions/reference/api/declarativeNetRequest](https://developer.chrome.com/docs/extensions/reference/api/declarativeNetRequest)
- Side Panel API — [https://developer.chrome.com/docs/extensions/reference/api/sidePanel](https://developer.chrome.com/docs/extensions/reference/api/sidePanel)
- Offscreen Documents API — [https://developer.chrome.com/docs/extensions/reference/api/offscreen](https://developer.chrome.com/docs/extensions/reference/api/offscreen)
- Scripting API — [https://developer.chrome.com/docs/extensions/reference/api/scripting](https://developer.chrome.com/docs/extensions/reference/api/scripting)
- Chrome Built-in AI — [https://developer.chrome.com/docs/ai/built-in](https://developer.chrome.com/docs/ai/built-in)
- Plasmo — [https://docs.plasmo.com/](https://docs.plasmo.com/)
- WXT — [https://wxt.dev/](https://wxt.dev/)
- vite-plugin-web-extension — [https://github.com/aklinker1/vite-plugin-web-extension](https://github.com/aklinker1/vite-plugin-web-extension)
- Mozilla WebExtensions — [https://extensionworkshop.com/](https://extensionworkshop.com/)
- Firefox MV3 — [https://extensionworkshop.com/documentation/develop/manifest-v3-migration-guide/](https://extensionworkshop.com/documentation/develop/manifest-v3-migration-guide/)
- Safari Web Extensions — [https://developer.apple.com/documentation/safariservices/safari_web_extensions](https://developer.apple.com/documentation/safariservices/safari_web_extensions)
- Edge Add-ons — [https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/](https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/)
- webextension-polyfill — [https://github.com/mozilla/webextension-polyfill](https://github.com/mozilla/webextension-polyfill)
- uBlock Origin Lite — [https://github.com/uBlockOrigin/uBOL-home](https://github.com/uBlockOrigin/uBOL-home)
- Chrome Web Store Developer Program Policies — [https://developer.chrome.com/docs/webstore/program-policies/](https://developer.chrome.com/docs/webstore/program-policies/)
- PlasmoHQ BPP (Browser Plugin Publisher) — [https://github.com/PlasmoHQ/bpp](https://github.com/PlasmoHQ/bpp)
- Yomitan (formerly Yomichan) — [https://github.com/yomidevs/yomitan](https://github.com/yomidevs/yomitan)
현재 단락 (1/739)
브라우저 익스텐션 개발자들은 5년 동안 "Manifest V3는 곧 강제된다"는 말을 들었다. 매번 연기되었다. 2022년 1월. 2023년 1월. 2024년 1월. 그리고 202...