Next-gen Web Extension Framework – WXT
WXT provides the best developer experience, making it quick, easy, and fun to develop web extensions. With built-in utilities for building, zipping, and publishing your extension, it's easy to get started.
wxt.dev
1단계: 브라우저 확장 개발 기본 이해
1. 브라우저 확장이란?
브라우저 확장(Extension)은 크롬, 엣지, 파이어폭스 같은 웹 브라우저의 기능을 확장하거나 사용자 경험을 향상시키기 위해 작성된 작은 웹앱입니다.
특징:
- HTML/CSS/JavaScript로 작성됨
- 브라우저 내부 API (chrome.*, browser.*)에 접근 가능
- 콘텐츠 페이지를 조작하거나 툴바 버튼, 메뉴 등을 추가 가능
예시:
- 광고 차단기 (AdBlock)
- 문법 검사기 (Grammarly)
- 유튜브 다운로더
- 개발자 도구 확장 (React DevTools)
2. Manifest 파일의 역할 (manifest.json 또는 manifest.ts)
Manifest는 확장의 구성과 권한, 실행방식을 정의하는 설정 파일입니다.
주요 정보:
- 이름, 설명, 버전
- 어떤 API를 사용할 수 있는지 (permissions)
- 어떤 스크립트를 언제 로드할지 (background, content_scripts)
- 어떤 UI를 제공할지 (popup, options_page, action 등)
예시:
{
"manifest_version": 3,
"name": "My Extension",
"version": "1.0",
"permissions": ["storage", "tabs"],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html",
"default_icon": "icon.png"
},
"content_scripts": [
{
"matches": ["https://*/*"],
"js": ["content.js"]
}
]
}
WXT에서는 이 JSON을 TypeScript에서 안전하게 정의합니다 (manifest.ts).
wxt.config.ts에서 manifest를 대신 할 때:
import { defineConfig } from 'wxt';
// See https://wxt.dev/api/config.html
export default defineConfig({
extensionApi: 'chrome',
modules: ['@wxt-dev/module-react'],
manifestVersion: 3, // manifest 외부에 작성
manifest: {
permissions: ['contextMenus', 'storage', 'activeTab', 'scripting', 'tabs'],
host_permissions: ['<all_urls>'],
name: '어디서든지 사용 가능한 단어장 sisyphus-word-extension',
version: '1.0',
action: {
default_popup: 'entrypoints/popup.html',
},
background: {
service_worker: 'entrypoints/background.ts',
type: 'module',
},
},
});
3. 주요 구성 요소의 역할
브라우저 확장은 여러 구성요소로 나뉘며 각각의 역할이 다릅니다.
.output/: 모든 빌드 아티팩트가 여기에 저장됩니다.
.wxt/: WXT에서 생성되었으며 TS 구성이 포함되어 있습니다.
assets/: WXT에서 처리해야 하는 모든 CSS, 이미지 및 기타 자산을 포함합니다.
components/: 기본적으로 자동 가져오기, UI 구성 요소 포함
composables/: 기본적으로 자동 가져오기, Vue용 프로젝트의 구성 가능한 함수에 대한 소스 코드 포함
entrypoints/: 확장 프로그램에 포함된 모든 진입점을 포함합니다.
hooks/: 기본적으로 자동 가져오기, React 및 Solid용 프로젝트 후크에 대한 소스 코드 포함
modules/: 프로젝트에 대한 로컬 WXT 모듈이 포함되어 있습니다.
public/: WXT에서 처리하지 않고 그대로 출력 폴더에 복사하려는 모든 파일을 포함합니다.
utils/: 기본적으로 자동 가져오기, 프로젝트 전체에서 사용되는 일반 유틸리티 포함
.env: 환경 변수 포함
.env.publish: 게시를 위한 환경 변수가 포함되어 있습니다.
app.config.ts: 런타임 구성이 포함되어 있습니다
package.json: 패키지 관리자가 사용하는 표준 파일
tsconfig.json: TypeScript의 동작 방법을 알려주는 구성
web-ext.config.ts: 브라우저 시작 구성
wxt.config.ts: WXT 프로젝트의 기본 구성 파일
Background Script (배경 스크립트)
- 항상 백그라운드에서 동작하는 스크립트
- 알람, 이벤트 리스너, 메시징 등을 처리
- MV3에서는 Service Worker로 실행됨 (항상 실행되진 않고 필요할 때만 활성화됨)
Content Script (콘텐츠 스크립트)
- 웹페이지의 DOM에 삽입되는 스크립트
- 사용자가 방문한 웹사이트 내부에 접근 가능
- 웹페이지의 HTML/CSS/JS 조작 가능
- 보안상 제한이 많음 (예: window.alert는 되지만 chrome.storage는 바로 접근 불가)
Popup Page (팝업 페이지)
- 브라우저 툴바 아이콘을 클릭했을 때 열리는 작은 HTML 페이지
- React UI를 붙이기 좋은 위치
- background와 메시지 통신하여 기능 수행
Options Page (설정 페이지)
- 확장 프로그램의 설정을 저장/편집하는 UI 페이지
- 일반적으로 chrome.storage에 값을 저장
- React로 정식 앱처럼 구성 가능
4. MV2 vs MV3의 차이점
MV2 (Manifest V2) — 기존 방식
- background.js는 항상 실행 상태
- 권한이 많고 자유도가 높지만 보안 및 성능에 단점이 있음
- Google이 지원 중단 예정 (2025년 완전 중단 예정)
MV3 (Manifest V3) — 최신 방식
- background.js 대신 Service Worker 사용 (필요할 때만 작동, 메모리 절약)
- 더 엄격한 보안 정책 (예: eval, remote script 불가)
- 퍼포먼스와 배터리 수명 향상
주요 차이 정리:
항목 | MV2 | MV3
| Background | 항상 실행됨 | 필요 시 활성화되는 Service Worker |
| 퍼포먼스 | 리소스 상시 사용 | 메모리 절약, 최적화 |
| 권한 및 보안 | 비교적 유연 | 매우 엄격한 Content Security Policy (CSP) |
| 지원 상황 | 지원 중단 예정 | 공식 권장 버전 (WXT도 MV3 기반) |
Vite + React vs WXT 차이 개요
1. 일반 Vite + React 앱 구조 (SPA)
특징:
- Single Page Application (SPA) 구조
- 진입점은 index.html 단 하나
- 모든 React 컴포넌트는 하나의 root에서 렌더링됨
- 페이지 전환은 React Router로 처리
폴더 예시:
src/
main.tsx ← React 앱 진입점
App.tsx
pages/
Home.tsx
About.tsx
index.html ← Vite가 빌드할 하나의 HTML 파일
vite.config.ts
실행 흐름:
- 브라우저는 index.html을 로드
- main.tsx가 React 앱을 마운트
- 앱 내 라우팅은 React Router에서 클라이언트 사이드로 처리
2. WXT 구조: 브라우저 확장을 위한 다중 엔트리 기반
WXT는 "브라우저 확장 개발"에 특화된 빌드 툴입니다. 하나의 SPA가 아니라, 여러 개의 독립된 entrypoint (진입점)가 존재합니다.
진입점 디렉토리를 사용하면 파일 entrypoints/{name}/index.{ext}옆에 관련 파일을 추가할 수 있습니다.
📂 entrypoints/
📂 popup/
📄 index.html ← This file is the entrypoint
📄 main.ts
📄 style.css
📂 background/
📄 index.ts ← This file is the entrypoint
📄 alarms.ts
📄 messaging.ts
📂 youtube.content/
📄 index.ts ← This file is the entrypoint
📄 style.css
예시 진입점들:
- popup.tsx → 툴바 팝업 UI
- options.tsx → 설정 페이지
- content.ts → 웹 페이지에 삽입되는 스크립트
- background.ts → 백그라운드에서 작동하는 서비스 워커
3. HTML 진입점 관리의 차이
Vite에서는:
- 수동으로 index.html을 만들고 설정함
WXT에서는:
- 각 entrypoint (popup.tsx, options.tsx)마다 자동으로 HTML이 생성됨
- 개발자가 직접 popup.html, options.html을 만들 필요 없음
- 대신 TypeScript 진입점을 만들고 React DOM으로 렌더링
// src/popup.tsx
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
즉,
- popup.tsx → dist/popup.html + popup.js
- options.tsx → dist/options.html + options.js
이런 식으로 WXT가 자동으로 HTML 파일들을 만들어줍니다.
4. 구조적 차이 핵심 요약
항목 | Vite + React (SPA) | WXT (브라우저 확장)
| 진입점 수 | 하나 (main.tsx) | 여러 개 (popup.tsx, content.ts 등) |
| HTML 구성 | 수동 (index.html) | 자동 생성 |
| 라우팅 방식 | React Router | 각 UI는 별도 HTML로 분리되어 페이지 자체가 분리됨 |
| 실행 환경 | 일반 웹 앱 | 브라우저 확장 환경 (MV3 기반) |
| 퍼포먼스 고려 | 앱 전역 번들링 | 기능별로 분리 번들링하여 가볍게 실행 |
WXT 개발 시 주의할 점
- React 앱처럼 모든 페이지를 라우터로 연결하지 않는다
→ 팝업, 옵션, 콘텐츠 등은 전혀 다른 entry로 동작 - HTML 파일을 만들지 않는다
→ WXT가 자동으로 처리 - Static 리소스는 public/이 아니라 static/ 폴더에 둔다
→ public 폴더는 WXT에서 무시됨 - Vite 설정을 직접 건드릴 일은 거의 없다
→ wxt.config.ts를 통해 확장에 필요한 설정만 조정
2단계: WXT 구조 및 설정 익히기
1. WXT의 기본 폴더 구조와 각 파일의 역할
WXT는 브라우저 확장 프로그램 개발에 특화된 구조를 제공합니다. 주요 구성 파일은 다음과 같습니다:
my-extension/
├── src/
│ ├── manifest.ts ← 확장 기능의 핵심 설정 (MV3 manifest를 타입으로 정의)
│ ├── background.ts ← 백그라운드 서비스 워커
│ ├── content.tsx ← 웹페이지에 삽입될 콘텐츠 스크립트 (DOM 접근용)
│ ├── popup.tsx ← 브라우저 툴바 클릭 시 열리는 팝업 UI
│ ├── options.tsx ← 설정 페이지 (옵션 페이지)
│ └── shared/ ← 공통 모듈 및 유틸 (예: 상태관리, API 등)
├── static/ ← 아이콘, 이미지, manifest에서 참조할 정적 자산
├── wxt.config.ts ← WXT 전용 설정 파일 (vite.config.ts와 유사한 역할)
├── package.json
└── tsconfig.json
2. 각 구성 요소 설명
src/manifest.ts
- 브라우저 확장의 모든 설정을 담는 파일
- JSON이 아니라 TypeScript로 정의하며 자동으로 manifest.json으로 변환됨
- 타입 자동 완성 및 오류 방지 가능
import { defineManifest } from 'wxt';
export default defineManifest({
name: 'My Extension',
manifest_version: 3,
version: '1.0.0',
action: {
default_popup: 'src/popup.html',
},
background: {
service_worker: 'src/background.ts',
type: 'module',
},
content_scripts: [
{
matches: ['https://*/*'],
js: ['src/content.tsx'],
},
],
options_ui: {
page: 'src/options.html',
open_in_tab: true,
},
permissions: ['storage'],
});
defineManifest()는 WXT가 제공하는 타입 안전 API로, 자동 HTML 경로 설정과 타입 체크를 제공합니다.
src/background.ts
- Service Worker로 동작하며 항상 백그라운드에서 실행됨
- 알람, 메시지 핸들링, 탭 제어 등 브라우저 API 작업 처리
chrome.runtime.onInstalled.addListener(() => {
console.log('Extension installed!');
});
src/content.tsx
- 웹 페이지에 삽입되는 스크립트 (DOM 접근)
- 필요하면 React로 UI를 삽입하거나 특정 요소를 조작 가능
- 보안 정책상 제한된 API만 사용 가능 (chrome.storage 직접 사용 불가)
src/popup.tsx
- React로 구성된 팝업 UI
- 브라우저 툴바 아이콘 클릭 시 표시
- 일반적인 React 앱처럼 상태 관리, API 호출 등을 처리
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
src/options.tsx
- 확장 프로그램의 설정 페이지
- chrome://extensions → 옵션 버튼 클릭 시 열리는 UI
- chrome.storage 등을 통해 사용자 설정 저장 가능
3. WXT의 자동 manifest 생성 방식
- WXT는 manifest.ts를 읽고, 내부적으로 manifest.json을 생성합니다.
- 각 진입점(popup.tsx, options.tsx, content.tsx)에 대해 자동으로 HTML 파일 생성 + Vite 번들 처리도 해줍니다.
- React 기반 UI를 직접 구성하면서도 HTML 파일을 수동으로 만들 필요 없음
예시:
// popup.tsx가 있으면 → popup.html 자동 생성됨
// content.tsx → content.js 자동 번들링되어 manifest에 등록됨
4. 타입 안전한 defineManifest() 사용
- defineManifest()는 WXT에서 제공하는 타입 안전한 manifest 정의 함수
- manifest_version, permissions, content_scripts 등의 속성에서 자동 완성 및 에러 방지 가능
- 실수로 틀린 필드명을 넣을 경우 컴파일 타임에 오류 발생
일반적인 manifest.json에서 발생할 수 있는 런타임 오류를 TypeScript 수준에서 막아줍니다.
5. WXT 프로젝트 초기화 방법
1. 프로젝트 생성
npm create wxt@latest
# 또는
pnpm create wxt@latest
2. 질문에 따라 선택
- 어떤 UI 엔트리포인트를 만들지 (popup, options, content 등)
- TypeScript 사용할 것인지
- React 사용할 것인지
- Tailwind 사용할 것인지
3. 개발 서버 실행
npm run dev
개발 서버를 실행하면 WXT가 각 진입점에 대해 HMR(Hot Module Replacement)을 적용해줍니다. 개발이 매우 빠릅니다.
3단계: React 통합 및 Vite 설정 주의점
1. popup.tsx 등 UI 진입점에서 React 렌더링 방식
WXT에서 React는 진입점별로 별도로 구성된 HTML 파일에 마운트됩니다. React 앱처럼 index.html을 직접 다루지 않고, WXT가 자동으로 popup.html, options.html 등을 생성합니다.
예시: popup.tsx
// src/popup.tsx
import ReactDOM from 'react-dom/client';
import App from './App';
const container = document.getElementById('root');
if (container) {
ReactDOM.createRoot(container).render(<App />);
}
이 방식은 일반 Vite + React 앱의 main.tsx와 매우 유사하지만, HTML은 WXT가 자동으로 생성한다는 점이 다릅니다.
WXT가 자동으로 생성하는 HTML 구조 (내부적으로)
<!-- 자동 생성된 popup.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Popup</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/popup.tsx"></script>
</body>
</html>
2. Vite + WXT의 차이점
항목 | Vite + React | WXT
| 진입점 | 하나: index.html | 여러 개: popup.tsx, options.tsx, content.tsx, ... |
| HTML | 수동 작성 | 자동 생성 (virtual HTML) |
| React 렌더링 | main.tsx 하나로 전체 앱 마운트 | 각 진입점마다 개별 React 앱 마운트 |
| 정적 자산 | public/ 폴더 사용 | static/ 폴더 사용 |
| 라우팅 | React Router | 라우팅 없음, 각 UI가 자체 페이지 |
3. 진입점별 virtual HTML 생성 방식
WXT는 Vite의 플러그인 시스템을 활용하여 HTML을 자동 생성합니다.
흐름:
- popup.tsx, options.tsx 등 진입점을 Vite entry로 등록
- WXT가 popup.html, options.html을 자동 생성하여 해당 TSX에 연결
- 빌드 시 HTML과 JS 번들이 함께 생성됨
→ 개발자가 html 파일을 신경 쓰지 않아도 됨
4. public 폴더는 사용되지 않음 → static/ 폴더 사용
기존 Vite + React 앱에서는 public/ 폴더에 아이콘, 이미지 등을 두면 정적 자산으로 /favicon.ico 식으로 접근할 수 있습니다.
하지만 WXT에서는 public/ 폴더를 무시하고 대신 static/ 폴더를 사용합니다.
예시:
static/
icon-48.png
icon-128.png
manifest.ts에서 사용:
default_icon: 'static/icon-48.png',
이때 경로는 실제로는 dist/static/icon-48.png에 복사되며, 자동으로 상대 경로로 처리됩니다.
5. CSS와 정적 자산 경로 처리 주의
CSS 적용 방식:
- 각 TSX 진입점에서 직접 import
// popup.tsx
import './styles/popup.css';
- Tailwind를 사용하는 경우에도 진입점별로 @tailwind base 등을 포함시켜야 함
⚠️ 콘텐츠 스크립트의 경우:
- 웹페이지에 삽입되는 코드이므로 전역 스타일 충돌 주의
- Shadow DOM을 사용하는 방식이 권장됨
// 예: content.tsx 내에서 shadow root 생성 후 React 마운트
const shadow = document.createElement('div');
shadow.attachShadow({ mode: 'open' });
document.body.appendChild(shadow);
// React root를 shadow 내부에 마운트
정적 이미지 등 사용 시:
React 내에서 직접 import 하거나, static/ 경로 기반으로 URL을 작성
// 방법 1: import 사용
import icon from '../static/icon-48.png';
<img src={icon} />
// 방법 2: 직접 경로 명시 (주의: 실제 빌드 경로 고려)
<img src="static/icon-48.png" />
4단계: Content Script에서 React 사용 시 유의사항
1. DOM 직접 접근/조작과 React의 충돌
Content Script는 웹사이트의 실제 DOM에 삽입되어 동작합니다.
문제 상황 예시:
- 이미 존재하는 <div id="root">와 겹쳐서 렌더링됨
- 기존 CSS 클래스와 Tailwind 클래스가 충돌
- 웹사이트의 JS가 직접 <body>를 변경하면 React 컴포넌트가 사라짐
해결 전략:
- 절대 웹사이트의 기존 DOM 요소를 직접 건드리지 말 것
- 가능한 한 독립적인 DOM 영역을 생성하여 React를 마운트해야 함
// content.tsx
const mountPoint = document.createElement('div');
mountPoint.id = 'my-extension-root';
document.body.appendChild(mountPoint);
2. Shadow DOM 사용 (스타일 격리)
Shadow DOM을 사용하면 확장 프로그램의 UI가 웹사이트의 스타일이나 DOM 구조와 완전히 분리되어 안전하게 렌더링됩니다.
장점:
- Tailwind나 CSS-in-JS의 클래스명이 웹페이지와 충돌하지 않음
- 사이트 스타일로 인해 버튼, 폰트 등이 망가지는 현상 방지
사용 예시:
// content.tsx
const container = document.createElement('div');
container.id = 'my-extension-shadow-host';
document.body.appendChild(container);
// Shadow Root 생성
const shadow = container.attachShadow({ mode: 'open' });
// React 마운트용 root 생성
const root = document.createElement('div');
shadow.appendChild(root);
// 스타일도 shadow 내부에 삽입
const style = document.createElement('style');
style.textContent = `
@import url("https://fonts.googleapis.com/css2?family=Inter&display=swap");
* { all: unset; font-family: 'Inter', sans-serif; }
`;
shadow.appendChild(style);
// React 렌더링
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(root).render();
3. 페이지 내 리소스 충돌 방지 (Tailwind 등)
Tailwind는 class 이름이 짧고 전역적(.text-sm, .bg-gray-100 등)이기 때문에, 웹페이지와의 충돌이 매우 흔합니다.
해결 방법:
- Shadow DOM 사용 (위에서 설명)
- Tailwind를 사용하는 경우, content script 전용 CSS를 별도로 만들어 삽입
import './styles/content.css'; // Shadow DOM 내부에 이 CSS를 삽입
Shadow DOM이 없다면 tailwind는 사용하지 않는 것이 안전합니다. WXT + content에서 tailwind는 고급 설정이 필요합니다.
4. insertReactRoot() 같은 유틸 함수 사용
WXT나 community에서는 위와 같은 과정을 단순화한 유틸 함수를 종종 사용합니다.
예시: insertReactRoot 유틸 함수
export function insertReactRoot(id = 'my-extension-root') {
const host = document.createElement('div');
host.id = id;
document.body.appendChild(host);
const shadow = host.attachShadow({ mode: 'open' });
const root = document.createElement('div');
shadow.appendChild(root);
return {
host,
shadow,
root, // 여기로 React 렌더링
};
}
사용 예:
import { insertReactRoot } from './utils/insert-react-root';
import ReactDOM from 'react-dom/client';
import App from './App';
const { root } = insertReactRoot();
ReactDOM.createRoot(root).render(<App />);
이 방식은 매번 반복해야 할 로직을 유틸로 모듈화해 코드 품질과 유지보수성을 높여줍니다.
5단계: Background Script / Service Worker 이해
1. Background는 React UI가 없음
- Background는 UI 없이 작동하는 비동기 처리 중심의 JavaScript 코드입니다.
- 주 역할은 이벤트 리스닝, 메시지 라우팅, 저장소 관리, 알림 트리거, 네트워크 요청 등입니다.
- MV3에서는 Background가 항상 실행되지 않고, 필요할 때만 실행되는 Service Worker 방식입니다.
예시 (MV3 - WXT 기준):
// src/background.ts
chrome.runtime.onInstalled.addListener(() => {
console.log('Extension installed');
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'PING') {
sendResponse({ type: 'PONG' });
}
});
2. 주요 API 사용법
WXT + MV3 환경에서 많이 사용하는 Background 관련 API를 정리합니다.
🔹 chrome.runtime.onInstalled
- 확장이 설치되거나 업데이트될 때 실행
chrome.runtime.onInstalled.addListener((details) => {
console.log('Installed or updated:', details.reason);
});
🔹 chrome.runtime.onMessage / sendMessage
- Popup, Content Script와 메시지를 주고받을 때 사용
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'FETCH_USER') {
// 비동기 응답
fetchUserFromServer().then((data) => {
sendResponse({ user: data });
});
return true; // 비동기 처리할 때 필수!
}
});
🔹 chrome.alarms — 주기적 타이머
- 백그라운드에서 반복 작업 예약
chrome.alarms.create('checkUpdates', { periodInMinutes: 5 });
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'checkUpdates') {
console.log('Checking updates...');
}
});
🔹 chrome.storage — 저장소 사용
- 유저 설정, 토큰, 캐시 등 저장 가능
chrome.storage.local.set({ theme: 'dark' });
chrome.storage.local.get('theme', (result) => {
console.log('Current theme:', result.theme);
});
3. Background에서의 상태관리 전략
React에서는 useState, Zustand 등을 사용하지만, Background는 UI가 없고 상태는 휘발성입니다.
전략 A: 스코프 변수 (휘발성)
let authToken: string | null = null;
chrome.runtime.onMessage.addListener((msg, _, sendResponse) => {
if (msg.type === 'SET_TOKEN') {
authToken = msg.token;
sendResponse({ success: true });
}
});
단점: Service Worker가 꺼지면 사라짐 (MV3의 특징)
전략 B: chrome.storage 사용 (지속성 보장)
chrome.runtime.onMessage.addListener((msg, _, sendResponse) => {
if (msg.type === 'SAVE_SETTINGS') {
chrome.storage.local.set({ settings: msg.payload }, () => {
sendResponse({ success: true });
});
return true; // 비동기 응답 시 필수
}
});
전략 C: IndexedDB 또는 Background Indexed 상태 캐시
- 복잡한 상태를 다룰 경우 IndexedDB를 사용하거나
- 이벤트 발생마다 chrome.storage에 바로 쓰지 않고 in-memory cache + flush 방식으로 처리
WXT 설정 팁
- WXT에서 background.ts는 자동으로 manifest에 등록되며, 다음처럼 작성합니다:
export default defineManifest({
background: {
service_worker: 'src/background.ts',
type: 'module',
},
});
React는 사용하지 않으며, 순수 TS/JS로 처리합니다. 필요한 경우 shared/에 유틸 함수들을 작성해 다른 진입점과 공유하세요.
6단계: 메시징 시스템 이해
1. 통신 흐름 구조 (전체 그림)
[content script] ⇄ [background] ⇄ [popup/options]
- Content Script는 웹페이지에 삽입된 코드 (DOM 접근 가능)
- Popup / Options는 React UI로 구성된 사용자 인터페이스
- Background는 중간 허브 역할 (메시지 라우팅, 권한 있는 API 실행 등)
각 환경은 완전히 분리된 context이므로, 동기적 데이터 공유가 불가능하고, chrome.runtime.sendMessage나 chrome.tabs.sendMessage로 통신해야 합니다.
2. 기본 메시지 API 사용법
🔹 popup → background
// popup.tsx
chrome.runtime.sendMessage({ type: 'GET_USER' }, (response) => {
console.log('User:', response);
});
// background.ts
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'GET_USER') {
sendResponse({ name: 'Heeseong', id: 1 });
}
});
비동기 처리를 하려면 반드시 return true를 호출해야 응답이 유효하게 유지됩니다.
🔹 background → content
- background는 특정 탭의 content script에게 메시지를 보낼 수 있음
// background.ts
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs[0]?.id) {
chrome.tabs.sendMessage(tabs[0].id, { type: 'PING' });
}
});
// content.ts
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'PING') {
console.log('Received ping');
}
});
🔹 content → background
// content.ts
chrome.runtime.sendMessage({ type: 'HIGHLIGHT' });
// background.ts
chrome.runtime.onMessage.addListener((msg) => {
if (msg.type === 'HIGHLIGHT') {
console.log('Page wants to highlight something');
}
});
3. 메시지 구조 설계 전략
메시지를 구조화하여 타입 안전하고 유지보수하기 쉬운 통신 체계를 만드는 것이 중요합니다.
예: 통일된 메시지 타입 설계
// types/messages.ts
export type Message =
| { type: 'PING' }
| { type: 'GET_USER' }
| { type: 'SAVE_SETTINGS'; payload: { theme: string } };
사용:
function sendMessage<T extends Message>(msg: T): Promise<any> {
return new Promise((resolve) => {
chrome.runtime.sendMessage(msg, resolve);
});
}
이렇게 하면 메시지 타입에 따라 구조가 명확해지고, 실수로 잘못된 메시지를 보내는 일이 줄어듭니다.
4. 메시지 흐름 예시 (정리)
Popup → Background → Content Script
↓ ↕ ↕
↓ chrome.runtime chrome.tabs.sendMessage
↓ ↕ ↕
chrome.runtime.sendMessage chrome.runtime.onMessage
🔁 메시지는 모두 비동기이며, 응답은 콜백으로 처리하거나 Promise 래핑이 필요합니다.
5. WXT에서 메시지 타입 안전하게 관리하기
- types/messages.ts에 공통 메시지 타입 선언
- sendMessage<T>() 함수 유틸로 감싸기
- onMessage 리스너도 switch-case 대신 타입 가드를 사용
// 유틸 예시
export function onMessage<T extends Message>(
handler: (msg: T, sender: chrome.runtime.MessageSender) => void
) {
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
handler(msg as T, sender);
return true;
});
}
7단계: 스토리지 및 퍼미션 처리
1. chrome.storage.local vs chrome.storage.sync
확장 프로그램에서는 로컬 저장소를 사용할 수 있습니다. 일반 웹앱의 localStorage와는 다르게, **확장 전용 스토리지 API인 chrome.storage**를 사용합니다.
chrome.storage.local
- 데이터는 사용자의 브라우저 로컬에 저장
- 용량: 5MB 이상
- 빠르며 동기화되지 않음
- 캐시, 사용자 설정 등에 적합
chrome.storage.local.set({ theme: 'dark' });
chrome.storage.local.get('theme', (result) => {
console.log(result.theme);
});
chrome.storage.sync
- Google 계정에 로그인된 경우, 설정이 동기화됨
- 브라우저 간에 자동으로 설정 공유됨
- 단점: 속도 느림, 저장 용량 제한 있음 (~100KB)
chrome.storage.sync.set({ preferredLang: 'en' });
일반적으로는 local을 쓰고, 사용자 설정처럼 꼭 동기화가 필요한 항목만 sync를 사용합니다.
2. 퍼미션 선언: permissions, host_permissions
Manifest에 정확한 퍼미션을 명시하지 않으면 API 사용 시 오류가 발생합니다.
예시: storage 퍼미션
// manifest.ts
export default defineManifest({
permissions: ['storage'],
});
- chrome.storage.local 또는 sync를 사용하려면 이 권한이 필요합니다.
host_permissions
- 콘텐츠 스크립트나 background에서 외부 사이트에 접근하려면 명시해야 합니다.
host_permissions: ['https://example.com/*'],
- 예를 들어 fetch()를 통해 외부 서버에 데이터를 보내려면 필수입니다.
예시: 전체 퍼미션 조합
export default defineManifest({
permissions: ['storage', 'tabs'],
host_permissions: ['https://*.google.com/*'],
});
3. Manifest 퍼미션과 실제 API 연동 예시
Manifest 설정
| permissions: ['storage'] | chrome.storage.local.get/set() |
| host_permissions: ['https://example.com/*'] | fetch(), tabs.query(), content script 주입 등 |
| permissions: ['tabs'] | 탭 정보 접근 (chrome.tabs.query) |
| permissions: ['alarms'] | 주기적 작업 예약 (chrome.alarms.create) |
예: 퍼미션 없을 때 오류
chrome.storage.local.set({ userId: 1 });
// → 퍼미션 누락 시: Unchecked runtime.lastError: Extension context invalid or missing permission
4. 저장 구조 설계 팁
- 가능한 한 명확한 키를 사용하여 구조화된 저장소 구성
- 모든 저장은 set() 한 번에 객체로 저장 가능
chrome.storage.local.set({
settings: {
theme: 'dark',
language: 'en',
},
});
- 필요한 경우 chrome.storage.onChanged로 변경 감지 가능
5. WXT에서 퍼미션 선언 위치
WXT에서는 manifest.ts에서 defineManifest() 내에 선언합니다:
export default defineManifest({
permissions: ['storage', 'tabs'],
host_permissions: ['https://api.example.com/*'],
});
주의:
- optional_permissions는 사용자가 승인할 수 있는 선택적 권한을 의미하며, 기본적으로는 접근 불가합니다.
8단계: 배포 및 테스트
1. wxt build로 프로덕션 번들링
WXT는 Vite 기반이기 때문에, 일반 React 앱처럼 dev와 build 명령을 가집니다.
# 개발용
npm run dev
# 프로덕션 번들
npm run build
실행 결과
- dist/ 폴더가 생성되며, 다음 항목들이 포함됨:
- manifest.json (타입 기반 자동 생성)
- popup.html, popup.js
- options.html, options.js
- background.js (service worker)
- static/ 폴더 (아이콘, 이미지 등)
2. zip 파일 생성 및 Chrome 웹 스토어 업로드
zip 압축 생성:
cd dist
zip -r ../my-extension.zip *
루트에 manifest.json이 바로 보이도록 압축해야 합니다. (dist 폴더 자체를 압축 ❌)
업로드:
- Chrome 웹 스토어 개발자 대시보드
- "새 항목 업로드"
- zip 파일 업로드
- 개인정보, 권한 설명 등 입력
- 심사 요청
참고:
- Google 심사는 보통 1~5일
- 민감한 권한(tabs, host_permissions)을 사용할 경우 설명이 부족하면 거부될 수 있음
3. 확장 프로그램 로컬 테스트 방법
1. 크롬에서 확장 프로그램 페이지 열기
- 주소창에: chrome://extensions/
- "개발자 모드" 활성화
2. "압축 해제된 확장 프로그램 로드"
- dist/ 폴더 선택
- manifest.json이 루트에 있는 폴더를 선택해야 함
3. 테스트 및 디버깅
- Popup → 아이콘 클릭 후 UI 확인
- Content Script → 대상 웹페이지 열고 console 확인
- Background → chrome://serviceworker-internals 또는 DevTools background 탭 확인
4. MV3의 Service Worker 갱신 문제 해결 방법
MV3 특징:
- background는 Service Worker로 동작함
- idle 상태가 되면 자동 종료
- 수정 후에도 캐시된 버전이 계속 실행될 수 있음
문제가 되는 상황:
- background.ts를 수정했는데 반영이 안 됨
- onMessage가 호출되지 않음
해결 방법:
개발자 모드에서 "업데이트" 버튼 누르기
- chrome://extensions 페이지에서 직접 클릭
브라우저 완전 재시작
- 크롬 완전 종료 후 다시 실행
버전 변경 → 강제 재로드 유도
- manifest.ts의 version을 1.0.1처럼 증가시키면 강제 재시작됨
export default defineManifest({
version: '1.0.1', // ← 변경 시 갱신됨
});
자동 재시작 방지 팁
- console.log 등으로 로딩 여부 확인
- 가능한 한 background 로직은 유틸 함수화하여 캐시 걱정 없는 구조 유지
9단계: 고급 주제 및 모듈화 전략
1. WXT에서 상태 관리 (Zustand / Jotai 등)
배경:
WXT는 여러 UI entrypoint (popup, options 등)와 백그라운드 context로 나뉘며, SPA처럼 하나의 전역 상태를 공유하지 않습니다.
문제:
- popup.tsx와 options.tsx는 서로 다른 페이지
- background.ts, content.tsx는 React가 아님
- Zustand, Jotai 등 클라이언트 상태관리 라이브러리는 각 entry에서 별도로 초기화됨
사용 전략: 저장소 + Zustand 조합
// src/shared/store/settings.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
type SettingsState = {
theme: 'light' | 'dark';
setTheme: (theme: SettingsState['theme']) => void;
};
export const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}),
{
name: 'settings',
getStorage: () => chromeStorageAdapter, // custom adapter (local → chrome.storage)
}
)
);
chrome.storage와 동기화하고 싶다면 custom storage adapter를 만들어 연결해야 합니다.
2. 여러 React 엔트리포인트 간 공유 코드 처리 (shared/ 폴더 활용)
구조 예시:
src/
popup.tsx
options.tsx
content.tsx
shared/
store/ ← Zustand, Jotai, 유틸 스토어
hooks/ ← 공통 커스텀 훅
components/ ← 버튼, Input, ThemeSelector 등
utils/ ← 메시지 처리, 파서, fetcher 등
types/ ← 공통 타입 (Message, User 등)
Tip:
- WXT는 alias 설정이 이미 잡혀 있으므로, @/shared/...처럼 편하게 import 가능
- absolute import (@/shared/...)는 폴더 이동에도 안전
3. TypeScript 활용 극대화 (예: 메시지 타입 추론 자동화)
목적:
- popup ↔ background, content ↔ background 통신 시, 메시지 타입을 엄격하게 추론
- 메시지 프로토콜을 선언적으로 관리
예: 메시지 스키마 정의
// shared/types/message.ts
export type MessageMap = {
GET_USER: { req: void; res: { name: string } };
SET_THEME: { req: { theme: 'light' | 'dark' }; res: void };
};
유틸 함수로 타입 안전한 메시지 발송
export function sendMessage<K extends keyof MessageMap>(
type: K,
payload: MessageMap[K]['req']
): Promise<MessageMap[K]['res']> {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ type, payload }, resolve);
});
}
background 리스너도 타입 안전하게 등록
chrome.runtime.onMessage.addListener(
<K extends keyof MessageMap>(
msg: { type: K; payload: MessageMap[K]['req'] },
sender,
sendResponse
) => {
if (msg.type === 'GET_USER') {
sendResponse({ name: 'Heeseong' });
}
return true;
}
);
이렇게 하면 메시지를 마치 API 호출하듯 안전하게 다룰 수 있어 규모가 커질수록 이점이 큽니다.
4. WXT의 플러그인 시스템 확장
WXT는 Vite 기반이기 때문에, 모든 Vite 플러그인을 사용할 수 있으며, 일부 확장 전용 플러그인도 존재합니다.
사용 예: WXT 구성 확장
// wxt.config.ts
import { defineConfig } from 'wxt';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
staticDir: 'static',
build: {
outDir: 'dist',
},
});
예시 플러그인:
- vite-plugin-inspect: 번들 구조 시각화
- vite-plugin-compression: 배포 시 gzip 압축
- vite-plugin-pwa: (적절히 구성 시) 오프라인 기능 확장
- vite-plugin-checker: TS, ESLint, VLS 등 개발 중 검사 강화
MV3 API 문서: Chrome Developers Docs
Chrome 확장 프로그램 | Chrome Extensions | Chrome for Developers
Chrome 확장 프로그램 개발 방법을 알아보세요.
developer.chrome.com
1. React Devtools 브릿지 추가
문제
- 브라우저 확장의 popup, options, content script는 별도의 iframe-like context에서 동작하므로 React DevTools가 기본적으로 동작하지 않을 수 있음.
해결책
- WXT의 각 React 진입점(popup.tsx, options.tsx, content.tsx)에서 React DevTools 브릿지를 명시적으로 로드
예시: 개발 환경에서만 React DevTools 브릿지 삽입
// src/shared/utils/load-devtools.ts
export function loadReactDevtools() {
if (import.meta.env.MODE === 'development') {
const script = document.createElement('script');
script.src = 'http://localhost:8097'; // React DevTools 브리지 서버
document.body.appendChild(script);
}
}
// popup.tsx, options.tsx 등에서 호출
import { loadReactDevtools } from '@/shared/utils/load-devtools';
loadReactDevtools(); // 개발 시에만 삽입
참고:
- DevTools 브리지를 띄우려면 터미널에서 다음 실행:
npx react-devtools
2. Shadow DOM + styled-components 자동 설정
문제
- content script에서 React를 Shadow DOM에 렌더링할 때 styled-components를 쓸 경우, 스타일이 Shadow DOM 내부로 적용되지 않음
해결책
- styled-components의 StyleSheetManager를 사용해 Shadow Root를 스타일 삽입 위치로 지정
예시
import { StyleSheetManager } from 'styled-components';
export function renderInShadow(shadowRoot: ShadowRoot, AppComponent: JSX.Element) {
const container = document.createElement('div');
shadowRoot.appendChild(container);
ReactDOM.createRoot(container).render(
<StyleSheetManager target={shadowRoot}>
{AppComponent}
</StyleSheetManager>
);
}
// content.tsx
const shadowHost = document.createElement('div');
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
document.body.appendChild(shadowHost);
renderInShadow(shadowRoot, <App />);
이 코드를 유틸 함수로 추출하면 모든 content script에서 일관되게 styled-components를 사용할 수 있습니다.
3. manifest.ts 자동 스키마 검사 및 문서화
목적
- manifest.ts에서 오타나 누락된 필드를 런타임 전에 잡고
- 자동으로 문서화 또는 JSON 변환까지 가능하게 만들기
방법
1. 타입 기반 자동 검사
WXT는 이미 defineManifest()에 @types/chrome 기반의 타입 시스템을 내장하고 있어, TS 오류로 사전 검출이 가능
2. Lint 강화 (예: ESLint + Zod + JSON Schema)
- zod를 사용해서 manifest.ts 구조를 선언
- JSON 변환 후 자동 문서 생성도 가능
3. 문서화 예시
// manifest-schema.ts
export const manifestSchema = z.object({
name: z.string(),
version: z.string().regex(/^\d+\.\d+\.\d+$/),
manifest_version: z.literal(3),
permissions: z.array(z.string()),
// ...
});
// manifest.ts
import { manifestSchema } from './manifest-schema';
const manifest = defineManifest({
name: 'My Extension',
version: '1.0.0',
manifest_version: 3,
permissions: ['storage'],
});
manifestSchema.parse(manifest); // 정적 검사
export default manifest;
4. JSON 문서화 자동 출력
import fs from 'fs';
fs.writeFileSync('manifest-doc.json', JSON.stringify(manifest, null, 2));
이 방식은 협업 시 매우 유용하며, CI에 문서 생성 자동화를 넣는 것도 가능합니다.
'Extension' 카테고리의 다른 글
| 서버 없이 이미지에서 텍스트를 추출 tesseract js (2) | 2025.03.24 |
|---|---|
| 크롬 확장프로그램 react로 개발하기 (WXT: Web Extension Framework) (0) | 2025.03.24 |
| chrome 확장 프로그램 (1) | 2025.03.24 |