고양이hyebin
React + TypeScript로 Chrome Extension 개발하기 (feat. CRXJS + Vite)
React + TypeScript로 Chrome Extension 개발하기 (feat. CRXJS + Vite)
February 4, 2024

개발 동기

회사에서 Electron으로 응용프로그램을 개발하여 크롬 탭 제어와 화면 제어 기능을 구현했지만, 여러 문제점에 직면했습니다.

  • 유지보수의 어려움
  • 업데이트 과정의 복잡성: 사용자 환경마다 다른 업데이트 이슈
  • 테스트의 한계: 다양한 환경에서의 테스트 어려움
  • 설치 문제: 노트북 기종, OS 버전별 호환성 이슈

이러한 문제점들을 해결하기 위해 다른 방법을 찾아보다 Chrome Extension으로도 크롬을 제어할 수 있다는 것을 알게되었어요. 가벼운 설치와 배포, 간편한 업데이트가 가능하기 때문에 react와 typescript를 이용하여 chrome extension을 만들어 보기로 했습니다.

프로젝트 구조

  src/
  ├── components/          # 공통 컴포넌트
  ├── hooks/              # 커스텀 훅
  └── pages/              # Extension 페이지들
      ├── background/     # Background Script
      │   └── index.ts
      ├── content/        # Content Script
      │   ├── App.tsx
      │   └── main.tsx
      ├── newtab/         # New Tab Override
      │   ├── NewTab.tsx
      │   ├── index.html
      │   └── index.tsx
      └── popup/          # Extension Popup
          ├── Popup.tsx
          ├── index.html
          └── index.tsx

페이지 아래 pages/ 폴더를 보면 background, content, newtab, popup으로 나뉘어 있는데요.

Chrome Extension의 주요 구성 요소는 다음과 같습니다.

  • Popup: 확장 프로그램 아이콘을 클릭하면 나타나는 UI
  • Content Scripts: 웹 페이지에 삽입되어 DOM을 제어하는 스크립트
  • Background: 브라우저 이벤트를 감지하고 컴포넌트 간 통신을 담당
  • New Tab (선택): Chrome의 기본 새 탭 페이지를 대체하는 커스텀 페이지

개발 환경 설치

1. Vite 프로젝트 생성

npm create vite@latest exe_app -- --template react-ts
cd exe-app
npm install

2. CRXJS 설치 및 특징

CRXJS는 Chrome Extension 개발을 위한 Vite 플러그인이에요.

npm i @crxjs/vite-plugin@beta -D

주요 특징

  • manifest.json 기반 자동 파일 인식 : CRXJS는 manifest.json을 파싱하여 확장 프로그램에 포함할 파일들을 자동으로 찾습니다.
  • Content Script에서도 작동하는 HMR : Content Script에서도 확장 프로그램의 상태를 유지하면서 즉시 UI가 업데이트됩니다.
  • Zero Configuration : Vite config에 플러그인만 추가하면 바로 개발을 시작할 수 있습니다.

3. Vite 설정

CRXJS 플러그인을 활성화하고, manifest.json 파일을 전달합니다.

// vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";

export default defineConfig({
  plugins: [
    react(),
    crx({ manifest }),
    (server: {
      port: 3000,
    }),
  ],
});

4. mainfest 설정

chrome extension의 핵심이라고 할 수 있는 파일인 manifest.json은 브라우저에 이 앱에 대한 정보를 알려주는 파일이에요. 이 파일을 통해 브라우저에 extension의 정보를 전달하고, extension으로 인식되도록 합니다.

아래 필드는 필수적으로 넣어줘야 하는 값들로, 하나라도 없으면 extension 앱으로 만들 수 없어요.

필수 설정

  • manifest_version: manifest 버전
  • version: 앱의 버전
  • name: 사용자에게 표시되는 chrome extension의 이름
  • action.default_popup: 확장 프로그램 아이콘 클릭 시 표시될 HTML 파일 경로

아이콘 설정 (옵션)

  • action.default_icon: 툴바에 표시될 확장 프로그램 아이콘
  • action.default_title: 아이콘에 마우스를 올렸을 때 표시되는 툴팁
{
  "manifest_version": 3,
  "version": "1.0.0",
  "short_name": "React App",
  "name": "Create React App",
  "action": {
    "default_icon": {
      "16": "images/icon16.png",
      "24": "images/icon24.png",
      "32": "images/icon32.png"
    },
    "default_title": "Click Me",
    "default_popup": "index.html"
  }
}

manifest.json을 직접 만들어도 되지만, 타입 안정성과 동적 버전 관리를 위해 TypeScript로 manifest.config.ts를 작성할 수 있습니다.

작동 방식

  1. 1.개발 시: manifest.config.ts 작성
  2. 2.vite.config.ts에서 import하여 CRXJS에 전달
  3. 3.빌드 시: CRXJS가 JavaScript 객체를 처리하여 최종 manifest.json 생성
  4. 4.배포 시: dist/manifest.json을 Chrome이 읽음
//manifest.config.ts

import { defineManifest } from "@crxjs/vite-plugin";

import packageJson from "./package.json";
const { version } = packageJson;

// 버전 문자열을 파싱하여 major.minor.patch.label로 분리
const [major, minor, patch, label = "0"] = version
  .replace(/[^\d.-]+/g, "")
  .split(/[.-]/);

// defineManifest: CRXJS가 제공하는 헬퍼 함수
// - 타입 자동완성 지원
// - 동적/비동기 manifest 정의 가능
// - env 파라미터로 빌드 환경 정보 접근 가능
export default defineManifest(async (env) => ({
  manifest_version: 3,
  name: env.mode === "staging" ? "[INTERNAL] Tools - EXE" : "Tools - EXE",
  description: "응용프로그램 EXE App",
  version:
    label === "0"
      ? `${major}.${minor}.${patch}`
      : `${major}.${minor}.${patch}.${label}`,
  version_name: version,
  action: {
    default_title: "popup",
    default_popup: "src/pages/popup/index.html",  // 아이콘 클릭 시 열릴 팝업 HTML 파일
  },
  chrome_url_overrides: {
    newtab: "src/pages/newtab/index.html",   // 새 탭을 열 때 표시될 커스텀 페이지 지정
  },
  icons: {
    "16": "e_image.png",
    "48": "e_image.png",
    "128": "e_image.png",
  },
  background: {
    service_worker: "src/pages/background/index.ts",
    type: "module",
  },
  content_scripts: [
    {
      matches: ["*://*/*"],
      js: ["src/pages/content/main.tsx"],
    },
  ],
  web_accessible_resources: [
    {
      resources: ["assets/js/*.js", "assets/css/*.css"],
      matches: ["*://*/*"],
    },
  ],
  permissions: ["storage", "scripting", "activeTab", "system.display", "tabs"],
}));

chrome extension 등록

manifest 설정이 완료되었으면, react 프로젝트를 빌드를 해야해요. 빌드된 산출물을 로컬 chrome 브라우저에 extension을 추가해 줍니다.

  1. 1.chrome://extensions/ 으로 들어가 우측 상단에 "개발자 모드"를 눌러줍니다.
  2. 2."압축해제된 확장 프로그램을 로드합니다." 를 클릭하여 빌드 dist 폴더를 선택합니다.
  3. 3.추가된 크롬 extension을 확인하고, 핀 고정을 클릭하면 default_pop에 설정한 index.html 이 보입니다.

참고