Skip to content

필사 모드: 브라우저 익스텐션 개발 2026 — Manifest V3 / Plasmo / WXT / Side Panel API / 사파리 익스텐션 심층 가이드

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

프롤로그 — 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...

작성 글자: 0원문 글자: 27,019작성 단락: 0/739