Skip to content
Published on

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

Authors

프롤로그 — 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 차이의 핵심

항목MV2MV3
Backgroundpersistent page / event pageService Worker (또는 Firefox의 event page)
webRequest blocking가능declarativeNetRequest로 대체 (Firefox 일부 예외)
Host permissionsinstall 시 자동 승인런타임 옵트인 가능(activeTab/optional_host_permissions)
Remote code허용금지 (모든 JS는 패키지에 포함)
Content Security Policy객체 형태더 엄격한 객체 형태
Action APIbrowser_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>
<html>
  <body>
    <script src="offscreen.js"></script>
  </body>
</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/storageuseStorage 훅 사용.
  • Messaging: 타입 안전한 메시징.
  • Cross-browser: Chrome / Firefox / Edge 빌드 분리.

Content Script UI 예시

// contents/inline.tsx
import type { PlasmoCSConfig } from 'plasmo'
import { useState } from 'react'

export const config: PlasmoCSConfig = {
  matches: ['https://*.example.com/*'],
}

export default function ContentUI() {
  const [count, setCount] = useState(0)
  return (
    <div style={{ position: 'fixed', top: 10, right: 10, background: 'white', padding: 12 }}>
      <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
    </div>
  )
}

이게 자동으로 페이지에 주입되어 동작한다. 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
import { defineConfig } from 'wxt'

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 한 줄 비교

항목PlasmoWXT
기반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
import { defineConfig } from 'vite'
import webExtension from 'vite-plugin-web-extension'
import path from 'path'

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를 반환하게 만들 수 있다.

import browser from 'webextension-polyfill'

await browser.storage.local.set({ key: 'value' })

Plasmo와 WXT는 이미 이 polyfill 또는 동등한 추상을 내장하고 있다.

manifest 차이

Firefox는 manifest.jsonbrowser_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)
import { useState } from '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 (
    <div className="flex flex-col h-screen p-4">
      <div className="flex-1 overflow-auto">
        {messages.map((m, i) => (
          <div key={i} className={m.role === 'user' ? 'text-blue-600' : 'text-gray-800'}>
            {m.content}
          </div>
        ))}
      </div>
      <div className="flex gap-2">
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && send()}
          className="flex-1 border p-2"
        />
        <button onClick={send} className="bg-blue-500 text-white px-4">
          Send
        </button>
      </div>
    </div>
  )
}
// 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로 헬퍼 함수 테스트
import { describe, it, expect, vi } from 'vitest'
import { syncData } from './background'

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

import { test, expect, chromium } from '@playwright/test'
import path from 'path'

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.executeScriptworld: '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