본문 바로가기
Extension

WXT 기반 (React) 확장 프로그램 개발

by curious week 2025. 6. 2.

https://wxt.dev/

 

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 개발 시 주의할 점

  1. React 앱처럼 모든 페이지를 라우터로 연결하지 않는다
    → 팝업, 옵션, 콘텐츠 등은 전혀 다른 entry로 동작
  2. HTML 파일을 만들지 않는다
    → WXT가 자동으로 처리
  3. Static 리소스는 public/이 아니라 static/ 폴더에 둔다
    → public 폴더는 WXT에서 무시됨
  4. 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을 자동 생성합니다.

흐름:

  1. popup.tsx, options.tsx 등 진입점을 Vite entry로 등록
  2. WXT가 popup.html, options.html을 자동 생성하여 해당 TSX에 연결
  3. 빌드 시 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에서 메시지 타입 안전하게 관리하기

  1. types/messages.ts에 공통 메시지 타입 선언
  2. sendMessage<T>() 함수 유틸로 감싸기
  3. 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 폴더 자체를 압축 ❌)

업로드:

  1. Chrome 웹 스토어 개발자 대시보드
  2. "새 항목 업로드"
  3. zip 파일 업로드
  4. 개인정보, 권한 설명 등 입력
  5. 심사 요청

참고:

  • 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에 문서 생성 자동화를 넣는 것도 가능합니다.