LabHub

블로그

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

한국어English日本語

프롤로그 — 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 버전이 올라갔다"는 게 아니다.

동시에 익스텐션 개발 도구도 진화했다. 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년 익스텐션 개발의 본질을 요약한다.

세 브라우저는 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초 정도 유휴 상태가 지속되면 브라우저가 종료시킨다. 다음 이벤트가 발생하면 다시 깨어난다.

이게 의미하는 바:

전형적인 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"]
    }
  }
]

규칙은 선언적이다. 익스텐션이 매 요청마다 결정을 내리지 않고, 브라우저가 규칙 집합을 보고 결정한다.

제약사항

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의 강점

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의 강점

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의 약점


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의 강점

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의 모든 것을 그대로 쓸 수 있다.

선택 기준


10장 · Firefox MV3 — 일부 MV2 유지

Mozilla는 Chrome의 MV3 전환에 미묘한 반대 입장이었다. 결과적으로 Firefox MV3는 Chrome MV3와 닮았지만 다른 부분이 있다.

Firefox MV3의 차별점

같은 코드를 양쪽에서 돌리기

// 공통 헬퍼
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의 특징

변환 도구 — safari-web-extension-converter

이미 만든 Chrome/Firefox 익스텐션을 Safari로 옮길 때:

xcrun safari-web-extension-converter /path/to/extension

이 명령이 Xcode 프로젝트 스캐폴드를 만들어준다. manifest를 읽고, 호환되지 않는 API들을 경고로 알려준다.

배포

이 진입장벽이 Safari 익스텐션 생태계가 Chrome만큼 풍성하지 않은 이유다. 하지만 iOS에서 익스텐션이 가능한 유일한 길이라 점점 중요해진다.


12장 · Edge Add-ons — Microsoft

Edge는 Chromium 기반이라 Chrome 익스텐션이 거의 그대로 동작한다. 하지만 Microsoft는 Edge Add-ons라는 자체 스토어를 운영한다.

Edge에 익스텐션 올리기

  1. Microsoft Partner Center에 개발자 계정 등록 (무료).
  2. 같은 .zip 패키지를 업로드.
  3. 검수.

Chrome 익스텐션과의 호환성

Edge에서만 가능한 것

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장 · 한국 / 일본 — 카카오, 네이버, ピクシブ

한국

일본

카카오/네이버 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/다.

한국·일본 익스텐션 개발의 현실


15장 · 누가 무엇을 골라야 하나 — 1인 / 팀 / 크로스 브라우저 / AI 통합

1인 개발자 + 빠른 프로토타입

소규모 팀 + 장기 프로덕트

크로스 브라우저 우선

AI 통합 익스텐션

광고 차단 / 콘텐츠 필터링

엔터프라이즈 / B2B


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 방어

개인정보


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를 안 쓰고 외부 결제로 우회하는 것 제한.

검수 시간

베타 채널 활용

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가 죽어서 작업이 끊긴다"

"WebSocket이 끊긴다"

"content script에서 페이지의 변수에 접근이 안 된다"

"Chrome에서 잘 되는데 Firefox에서 안 된다"

"검수에서 거절됐다 — 권한이 과도하다"

"익스텐션이 갑자기 비활성화됐다 — 정책 위반"


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

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다