Wxt 완벽 가이드 — 모던 브라우저 확장 프로그램 개발(Manifest V3·TypeScript·Vite)
이 글의 핵심
Wxt는 Vite 기반으로 Manifest를 생성·동기화하고, entrypoints 규칙으로 팝업·백그라운드·콘텐츠 스크립트를 일관되게 번들하는 차세대 브라우저 확장 프레임워크입니다. 이 글에서는 핵심 개념, MV3, TS/Vite 통합, 스토리지·메시징, 멀티 브라우저, 실전 예제까지 한 흐름으로 다룹니다.
이 글의 핵심
Wxt(Web Extension Toolchain)는 브라우저 확장 프로그램을 현대 프론트엔드 도구와 같은 속도로 개발하기 위한 빌드·구조 규칙·매니페스트 생성을 통합한 프레임워크입니다. 수동으로 manifest.json을 맞추고, 콘텐츠 스크립트 경로를 일일이 연결하고, 번들러 설정을 확장 전용으로 늘리던 과정을 entrypoints/ 규칙과 wxt.config.ts 한곳으로 모읍니다.
이 글은 Manifest V3(MV3) 를 전제로 TypeScript·Vite와의 관계, 콘텐츠 스크립트와 백그라운드(서비스 워커) 역할 분리, storage와 메시징, 크로스 브라우저 빌드, 마지막으로 실전 확장을 예시로 연결합니다. 독자는 npm·TypeScript 기본기와 브라우저 확장의 대략적인 개념(권한, 격리된 월드)이 있으면 설명의 밀도를 온전히 활용할 수 있습니다.
1. Wxt가 해결하는 문제
전통적으로 확장 프로그램 저장소는 여러 진입점(백그라운드, 팝업, 옵션, 콘텐츠 스크립트, 오프스크린·사이드패널 등)이 서로 다른 실행 환경에 놓입니다. 웹 페이지와 달리 CSP·권한·웹 접근 가능 리소스 선언이 빌드 산출물과 항상 일치해야 하고, MV3 이후에는 백그라운드가 지속 프로세스가 아닌 서비스 워커로 바뀌어 수명 모델까지 달라졌습니다.
Wxt는 이 복잡도를 “엔트리포인트 이름 규칙 + 설정에서의 매니페스트 병합”으로 흡수합니다. 즉, 개발자는 기능 단위 파일에 집중하고, 매니페스트 조각은 각 엔트리의 defineBackground, defineContentScript, HTML의 <meta name="manifest.*"> 등으로 선언합니다. 빌드 시 Vite가 각 진입점을 번들하고, Wxt가 최종 manifest.json을 생성합니다.
1.1 핵심 개념 정리
| 개념 | 설명 |
|---|---|
| 엔트리포인트 | entrypoints/ 아래 단일 파일 또는 index가 있는 디렉터리. 이름이 팝업·백그라운드·*.content 등 타입을 결정합니다. |
| 선언적 매니페스트 | JSON을 손으로 편집하기보다 코드·메타 태그로 권한·매치 패턴·실행 시점을 기술합니다. |
| Listed / Unlisted | 매니페스트에 직접 등록되는 자산과, runtime.getURL로만 참조하는 비등록 페이지·스크립트를 구분합니다. |
| 브라우저 추상화 | webextension-polyfill 스타일의 browser API를 사용하면 Chrome·Firefox 등에서 시그니처 차이를 줄일 수 있습니다. Wxt 문서도 이를 전제로 한 예제가 많습니다. |
엔트리 관련 보조 파일은 반드시 해당 엔트리 디렉터리 안에 두어야 합니다. entrypoints/ 루트에 임의의 .ts를 두면 Wxt가 별도 엔트리로 오인하여 빌드 오류로 이어질 수 있습니다.
2. Manifest V3 지원
Chrome을 중심으로 MV3가 표준이 되면서, 백그라운드는 service_worker 단일 스크립트, 원격 호스트 코드 제한, 선언적 네트워크 요청 등 규칙이 강화되었습니다. Wxt는 타깃 브라우저와 매니페스트 버전에 맞춰 산출물을 조정하며, 설정에서 manifest 필드로 수동 병합할 수 있습니다.
2.1 MV3에서 달라진 실행 모델
- 서비스 워커 수명: 유휴 시 종료될 수 있으므로 전역 상태에 의존하지 않는 설계가 필요합니다.
main의 동기 제약: Wxt의defineBackground에서 백그라운드main은 비동기일 수 없다는 점이 문서화되어 있습니다. 초기화는 동기적으로 등록하고, 실제 비동기 작업은 리스너 내부에서 처리합니다.- 권한 최소화:
host_permissions·permissions를 실제 사용 API에 맞게 줄이는 것이 심사·보안 모두에 유리합니다.
2.2 wxt.config.ts에서 매니페스트 다루기
manifest는 객체로 두거나, 브라우저·모드 등에 따라 함수로 반환해 조건부 필드를 넣을 수 있습니다. 아이콘은 public/의 icon-16.png 같은 이름 규칙으로 자동 발견되는 경우가 많고, 이름·버전은 package.json에서 기본 상속됩니다.
import { defineConfig } from 'wxt';
export default defineConfig({
manifest: {
name: 'MV3 예제',
permissions: ['storage', 'tabs'],
host_permissions: ['https://example.com/*'],
},
// 필요 시: manifest: ({ browser, manifestVersion, mode, command }) => ({ ... })
});
위 설정은 프로젝트 전역 권한과 메타를 한곳에 모으는 패턴입니다. 반면 특정 사이트에만 주입하는 콘텐츠 스크립트는 엔트리의 matches에 두는 편이 유지보수에 유리합니다. 이렇게 전역 설정과 엔트리 설정의 책임을 나누면 팀원이 매니페스트를 읽지 않고도 소스 트리만으로 의도를 추적할 수 있습니다.
3. TypeScript와 Vite 통합
Wxt는 내부적으로 Vite를 사용합니다. 따라서 경로 별칭, Vue/React/Svelte 등 프레임워크, CSS 전처리기, 환경 변수는 Vite 관례를 따르되, 확장 프로그램 맥락(여러 진입점·서비스 워커·격리된 콘텐츠)에 맞게 Wxt가 플러그인과 해석 규칙을 추가합니다.
3.1 TypeScript의 이점
- 메시지 페이로드·스토리지 스키마를 타입으로 고정하면 콘텐츠·백그라운드 간 계약이 명확해집니다.
browser.*API는@types/chrome또는 프로젝트에 맞는 타입 패키지와 함께 쓰면 자동 완성으로 실수를 줄일 수 있습니다.
3.2 빌드·개발 명령
일반적으로 wxt CLI로 개발 서버(확장 리로드 포함)·프로덕션 빌드를 실행합니다. 산출물은 .output/<타깃>/ 형태로 정리되며, 스토어 제출용 zip은 해당 디렉터리를 패키징하면 됩니다. package.json에는 보통 dev, build, zip, postinstall의 wxt prepare 등이 올라가며, Firefox는 -b firefox 플래그로 별 타깃을 둡니다.
3.3 자동 임포트와 타입
Wxt는 defineBackground, defineContentScript 등 엔트리 헬퍼를 별도 import 없이 쓸 수 있게 구성하는 것이 일반적입니다. wxt prepare를 실행하면 .wxt/tsconfig.json, wxt.d.ts가 생성되어 TypeScript가 전역 심볼을 인식합니다. 아래 코드 예제는 이 관례에 맞춰 import 문을 생략합니다.
4. 프로젝트 구조와 엔트리포인트
4.1 기본 레이아웃(개념도)
프로젝트/
entrypoints/
background.ts → 서비스 워커(MV3)
popup/index.html → 툴바 팝업
example.content.ts → 이름 기반 콘텐츠 스크립트
public/ → 아이콘 등 정적 자산
wxt.config.ts
package.json
깊은 중첩 엔트리는 Next.js의 pages처럼 무한히 들어가지 않습니다. 문서에 따르면 0~1단계 깊이에서 발견 규칙이 성립합니다. 여러 사이트용 스크립트는 youtube.content/, github.content/처럼 접미사 .content로 나누는 패턴이 일반적입니다.
4.2 백그라운드: defineBackground
MV3에서는 출력이 서비스 워커로 연결됩니다. 모듈 최상단에서 브라우저 API를 직접 호출하지 말고, Wxt가 권장하듯 main 내부(또는 콜백)에서 리스너를 등록합니다. 빌드 시 엔트리 파일이 Node 환경에서 한 번 로드되기 때문입니다.
export default defineBackground(() => {
browser.runtime.onInstalled.addListener(() => {
console.log('확장 설치 또는 업데이트');
});
});
서비스 워커는 언제든 중지될 수 있으므로, “시작 시 한 번만” 의존하는 로직은 이벤트 기반으로 옮기고, 영속 데이터는 storage API에 둡니다.
4.3 콘텐츠 스크립트: defineContentScript
matches로 주입 URL을 제한하고, runAt으로 실행 시점을 잡습니다. 격리된 월드(ISOLATED) 가 기본이며, 페이지의 window와 직접 공유되지 않습니다. 페이지 스크립트와 상호운용이 필요하면 MAIN 월드·주입 스크립트 등 별도 패턴을 검토해야 합니다.
export default defineContentScript({
matches: ['https://example.com/*'],
runAt: 'document_idle',
main(ctx) {
const el = document.querySelector('h1');
if (el) el.setAttribute('data-wxt', '1');
},
});
main은 비동기 함수로 선언할 수 있다는 점이 백그라운드와 다릅니다. DOM 준비·네트워크 이후 작업을 async/await로 표현하기 쉽습니다.
5. Content Scripts와 Background의 역할 분담
실무에서 가장 흔한 분업은 다음과 같습니다.
| 역할 | 콘텐츠 스크립트 | 백그라운드(서비스 워커) |
|---|---|---|
| DOM 접근 | 직접 가능 | 불가(페이지 문서에 접근하려면 콘텐츠 또는 오프스크린 등 별도 경로) |
| 탭·북마크·알람 | 메시지로 위임하는 경우가 많음 | tabs, alarms 등 브라우저 API 호출에 적합 |
| 수명 | 탭·문서와 연동 | 이벤트 때마다 기동·종료 반복 가능 |
설계 원칙: 콘텐츠 스크립트는 가벼운 관찰·UI 오버레이에 집중하고, 권한이 큰 작업·집계·스케줄링은 백그라운드로 옮깁니다. 그렇지 않으면 콘텐츠 스크립트가 모든 사이트에 불필요하게 무거워지고, 성능·심사 리스크가 커집니다.
6. Storage와 메시징
6.1 Storage
chrome.storage.local / sync는 용량·동기화 정책이 다릅니다. 사용자 설정처럼 기기 간 동기화가 의미 있으면 sync, 대용량·민감도가 높으면 local을 고릅니다. MV3에서도 비동기 스토리지 API를 사용하는 것이 권장됩니다.
타입 안전성을 높이려면 키 상수와 저장 구조를 한 모듈로 두고, 콘텐츠·팝업·백그라운드에서 동일하게 import합니다.
// 예: storage 키와 기본값 (개념 예시)
export const STORAGE_KEYS = {
enabled: 'featureEnabled',
} as const;
export type Options = {
enabled: boolean;
};
export async function loadOptions(): Promise<Options> {
const { [STORAGE_KEYS.enabled]: enabled = true } =
await browser.storage.sync.get(STORAGE_KEYS.enabled);
return { enabled: Boolean(enabled) };
}
6.2 메시징
표준 API는 runtime.sendMessage, tabs.sendMessage, onMessage입니다. 타입과 에러 처리가 번거롭다면 Wxt 문서가 권장하는 래퍼를 고려할 수 있습니다.
- @webext-core/messaging: 가볍고 타입 안전한 메시지 래퍼
- webext-bridge: 채널 기반으로 손쉬운 라우팅
- trpc-chrome: tRPC 사용자에게 친숙한 RPC 스타일
// 백그라운드 (개념 예시)
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message?.type === 'GET_TAB_ID') {
sendResponse({ tabId: sender.tab?.id });
return true; // 비동기 응답 시 true
}
});
콘텐츠 스크립트에서 특정 탭 컨텍스트로 보낼 때는 tabs.sendMessage가 필요하고, 팝업에서 백그라운드로는 runtime.sendMessage가 흔합니다. 응답이 비동기일 때 Chrome 계열에서는 sendResponse와 return true 패턴을 정확히 맞춰야 합니다.
7. 크로스 브라우저 지원
Wxt는 Chrome, Firefox, Edge, Safari 등 여러 타깃으로 빌드할 수 있는 워크플로를 전제로 합니다. 다만 Safari는 Web Extension 패키징·서명 절차가 별도이고, Firefox는 sidebar_action vs Chrome의 side_panel처럼 API 이름이 다른 경우가 있습니다. Wxt는 사이드패널 등 일부 엔트리에서 브라우저별 매니페스트 매핑을 문서화합니다.
실무 체크리스트:
webextension-polyfill또는 동등한 추상화로 Promise 스타일 통일- 권한 이름 차이(
browser_actionvsaction등)는 빌드 타깃별 분기 - 스토어 규정: Chrome 웹스토어·Firefox AMO·Edge 애드온 각각의 권한 설명·개인정보 처리 요구사항 충족
- Safari: Xcode·네이티브 래퍼와의 연계가 필요할 수 있음
manifest.include / manifest.exclude 메타로 특정 브라우저에만 엔트리를 넣는 방법도 활용할 수 있습니다.
8. 실전: “현재 탭 정보 표시” 확장 구축
목표는 팝업에서 활성 탭의 제목·URL을 읽어 표시하는 것입니다. URL·탭 정보는 tabs 권한이 필요하며, 사용자 프라이버시를 위해 필요한 최소 범위만 요청합니다.
8.1 권한과 매니페스트 조각
wxt.config.ts에 tabs를 추가하고, host_permissions는 선택 사항입니다(특정 사이트만 읽을 때만 좁힙니다).
8.2 백그라운드: 탭 조회
export default defineBackground(() => {
browser.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
if (msg?.type !== 'GET_ACTIVE_TAB') return;
browser.tabs.query({ active: true, currentWindow: true }).then(([tab]) => {
sendResponse({ title: tab?.title, url: tab?.url });
});
return true;
});
});
tabs.query는 비동기이므로 return true로 응답 채널을 유지합니다. 이런 패턴은 메시징에서 가장 흔한 실수 중 하나이므로 팀 가이드에 넣기 좋습니다.
8.3 팝업 UI에서 호출
팝업은 일반적인 HTML+Vite 진입점입니다. 스크립트에서:
const res = await browser.runtime.sendMessage({ type: 'GET_ACTIVE_TAB' });
document.getElementById('out')!.textContent = `${res.title}\n${res.url}`;
8.4 확장하기
- 콘텐츠 스크립트로 페이지에서 선택 텍스트를 보내고 백그라운드에서 번역 API를 호출
storage에 사용자 옵션 저장 후 콘텐츠 주입 on/offalarms로 주기적 작업(단, 서비스 워커 수명을 고려해 설계)
각 단계마다 권한을 늘리는 이유를 스토어 설명에 명시하면 심사 대응이 수월합니다.
9. 모범 사례와 흔한 실수
- 엔트리 최상단 부작용 금지: 빌드 시 Node에서 로드되므로, 런타임 API 호출은
main안으로. - 서비스 워커 상태 의존 금지: 메모리 대신
storage. - 메시지 프로토콜 버전화:
type문자열만 쓰다 필드가 늘면 호환성 깨짐 →v필드 또는 스키마 버전. - 콘텐츠 스크립트 성능:
document_idle우선, 무거운 작업은 청크·requestIdleCallback등 고려. - 웹 접근 가능 리소스: 페이지에 노출하는 JS/CSS는
web_accessible_resources선언 누락 여부를 빌드 산출물에서 확인.
10. 정리
Wxt는 “확장 프로그램을 하나의 제품”으로 다루기 위해 엔트리 규칙·매니페스트 생성·Vite 번들을 한 toolchain으로 묶습니다. MV3 환경에서는 서비스 워커 수명과 권한 최소화가 아키텍처를 지배하고, 콘텐츠 스크립트와 백그라운드의 역할 분담이 유지보수 비용을 좌우합니다. Storage로 상태를 영속화하고, 메시징으로 컨텍스트를 연결하며, 크로스 브라우저는 타깃별 빌드와 스토어 정책을 전제로 설계하면 프로덕션 품질에 한층 가까워집니다.
공식 문서의 Entrypoints, Manifest, Messaging 장을 함께 보면 세부 옵션(registration, cssInjectionMode, world 등)을 프로젝트 요구에 맞게 미세 조정할 수 있습니다. 배포 전에는 .output 산출물의 manifest·권한·스크립트 경로를 diff로 검토하는 습관을 권장합니다.
참고
- Wxt 공식 문서: https://wxt.dev
- Chrome 확장 MV3 개요: https://developer.chrome.com/docs/extensions/mv3/intro/
- WebExtensions 메시징: Mozilla 문서의 Content scripts ↔ background 통신 절 참고