May 4, 2024 ˑ 10min read
“React.js로 개발하는 크롬 확장 프로그램” 앞선 두 파트에서는 크롬 확장 프로그램을 구성하는 요소들에 대해 알아보았습니다. 이번 글에서는 Vite와 React.js를 이용하여 크롬 확장 프로그램 개발 환경을 구성하고, Chrome API를 사용하는 간단한 확장 프로그램을 만들어보겠습니다.
Pnpm과 create-vite
를 사용하여 프로젝트를 초기화합니다.
pnpm create vite chrome-extension-with-react-vite --template react-ts
cd chrome-extension-with-react-vite
pnpm install
프로젝트 루트에 확장 프로그램을 위한 manifest.json
파일을 생성한 뒤 필요한 정보를 입력합니다.
{
"$schema": "https://json.schemastore.org/chrome-manifest",
"manifest_version": 3,
"name": "chrome-extension-demo",
"version": "1.0",
"description": "크롬 확장 프로그램 데모",
...
}
Tip
$schema
키를 통해 JSON 스키마 URL을 등록하면 IDE에 따라 자동완성 기능을 사용할 수 있습니다.
디렉토리는 크롬 확장 프로그램의 요소별로 생성하여 관리합니다.
src/
├── background/ # 백그라운드 스크립트
│ └── index.ts
└── popup/ # 팝업
├── components/
│ ├── Button.jsx
│ ├── Modal.jsx
│ └── ...
├── index.html
└── index.ts
백그라운드 스크립트와 팝업 요소 이외에 필요한 요소가 있다면 별도의 디렉토리를 생성해줍니다.
크롬 확장 프로그램은 보안상 목적으로 chrome://
스킴을 사용하여 동작하기 때문에 아쉽게도 https://
를 통해 페이지를 요청해야 하는 vite의 개발 서버를 사용한 환경 구성은 어려움이 있습니다.
대신 vite build --watch
명령을 통해 파일이 변경될 때 마다 빌드를 실행하는 방식으로 개발 환경을 구성할 수 있습니다.
create-vite
보일러플레이트는 루트에 있는 index.html
파일을 진입점으로 설정하기 때문에 확장 프로그램 요소별로 빌드가 될 수 있도록 진입점을 설정해주어야 합니다. 또한, manifest.json
파일에 빌드 결과물이 등록될 수 있도록 파일명에 hash 값이 포함되지 않도록 합니다.
import path from 'path';
import fs from 'fs'
export default defineConfig({
...
build: {
rollupOptions: {
input: {
popup: path.resolve('src', 'popup', 'index.html'),
background: path.resolve('src', 'background', 'index.ts')
},
output: {
entryFileNames: 'src/[name]/index.js',
}
}
}
})
이렇게 설정하고 vite build --watch
를 실행하면 dist/src
디렉토리 내 요소별로 빌드된 결과물을 확인할 수 있습니다.
이렇게 생성된 결과물을 크롬 확장 프로그램에서 사용할 수 있도록 manifest.json
을 수정합니다.
{
...
"permissions": ["tabs"], // 탭 정보를 얻기 위해 권한 필요
"action": {
"default_icon": {
...
},
"default_popup": "src/popup/index.html"
},
"background": {
"service_worker": "src/background/index.js"
}
}
그리고 파일이 수정될 때 마다 빌드가 실행되면서 위에서 작성한 manifest.json
이 결과물에 포함되어야 하는데요.
간단한 Vite Plugin을 통해 파일이 복사되도록 구성할 수 있습니다.
import path from 'path';
import fs from 'fs'
import { Plugin } from 'vite'
const manifestFile = path.resolve('manifest.json')
const outTarget= path.resolve('dist', 'manifest.json')
const CopyManifest = (): Plugin => {
return {
name: 'copy-manifest-plugin',
buildStart() {
// `manifest.json`을 감시 대상에 추가
this.addWatchFile(manifestFile)
},
writeBundle() {
// `manifest.json`을 복사
fs.copyFileSync(manifestFile, outTarget)
}
}
}
Notevite plugin에 대한 내용은 Vite 플러그인 알아보기나 vite 공식 문서 를 확인해주세요.
다시 vite build --watch
를 실행하면 크롬 브라우저에 확장 프로그램을 로드할 준비를 마치게 됩니다.
크롬 브라우저를 열고 주소창에 chrome://extensions
를 입력하여 확장 프로그램 관리 페이지로 이동합니다.
우측 상단에 있는 개발자 모드를 토글하면 “압축해제된 확장 프로그램을 로드합니다.” 버튼이 노출되는데요. 이 버튼을 통해 dist
디렉토리를 열어주면 확장 프로그램이 로드됩니다.
확장 프로그램 로드에 성공하면 브라우저 액션 영역에 등록한 아이콘 버튼이 노출됩니다. (버튼이 노출되지 않는다면 퍼즐 모양의 메뉴를 통해 핀(pin)을 등록해주세요.)
팝업 노출까지 확인했으니 백그라운드 스크립트와 메세지를 주고 받을 수 있도록 코드를 수정해보겠습니다.
우선 TypeScript 환경에서 chrome
네임스페이스를 통해 Chrome API에 접근하기 위해 @types/chrome
를 설치해줍니다.
pnpm add -D @types/chrome
NoteTypesScript 설정에
compilerOptions.types
를 구성하지 않았다면@types/*
의존성은 자동으로 적용됩니다. 만약 해당 설정이 되어있다면compilerOptions.types
설정에chrome
을 추가해주세요.
먼저 백그라운드 스크립트에 메세지 이벤트 핸들러를 등록하고, 메세지를 받으면 탭 정보를 응답하도록 구성해보았습니다.
chrome.runtime.onMessage.addListener((message, _, sendResponse) => {
const responseTabs = async () => {
const tabs = await chrome.tabs.query({});
sendResponse(tabs);
};
switch (message) {
case "get-tab-info": {
responseTabs();
return true;
}
default:
console.warn(`알수없는 메세지입니다. - ${message}`);
}
});
백그라운드 스크립트의 핸들러에서 한 가지 유의해야할 점은 async/await
문을 핸들러에서 바로 사용하면 안된다는 것인데요.
백그라운드 스크립트는 서비스 워커를 통해 실행되므로 핸들러 함수가 종료되면 유휴 상태로 변경될 수 있습니다. 때문에 메세지를 응답받는 UI 요소에서는 undefined
를 응답받을 수 있습니다.
이러한 경우를 위해 Chrome API의 이벤트 핸들러에서는 true
값을 반환하면 아직 실행중인 비동기 함수가 있음을 감지하고, 비동기 함수가 끝날때 까지 기다려주는 동작을 제공합니다.
따라서 핸들러 함수에서 비동기 함수를 통해 메세지를 응답해야 하는 경우 별도의 비동기 함수를 통해 async/await
문을 사용하고 핸들러에서는 true
값을 반환해주어야 합니다.
핸들러를 추가하였으니 팝업에서 메세지를 보내 탭 정보를 가져와보도록 하겠습니다.
import { useState } from "react";
import "./App.css";
function App() {
const [tabs, setTabs] = useState<chrome.tabs.Tab[]>([]);
const handleClick = async () => {
const nextTabs: chrome.tabs.Tab[] = await chrome.runtime.sendMessage("get-tab-info");
setTabs(nextTabs);
};
return (
<>
<div>
<div>
{tabs.map((tab) => (
<p key={tab.id}>{tab.title || tab.url}</p>
))}
</div>
<button onClick={handleClick}>탭 정보 불러오기</button>
</div>
</>
);
}
export default App;
TipChrome API는 Manifest V3부터
Promise
를 지원합니다. 콜백 함수를 등록하지 않고 응답을 받아올 수 있습니다.
코드를 수정하고 확장 프로그램 관리 화면에서 새로고침하면 결과를 확인해볼 수 있습니다.
확장 프로그램 등록까지 성공했지만, Vite의 HMR(Hot Module Replacement)를 사용할 수 없기 때문에 코드를 변경하고 매번 새로고침을 해주어야 하는 문제가 남아있습니다. 이를 해결할 수 있는 방법이 있는지 찾아보던 중 아래 프로젝트를 발견했습니다. (심지어 오너가 한국인이십니다..🇰🇷)
이 보일러플레이트 프로젝트는 Vite의 HMR과 같이 내부적으로 브라우저와 웹소켓을 연결하여 파일이 변경되었을 때 리로드(reload)하는 기능을 제공합니다. 레거시 버전에서는 확장 프로그램의 전체 요소를 다시 빌드하는 구조였지만, 리뉴얼 버전부터 Turborepo를 이용하여 확장 프로그램의 각 요소를 별개의 워크스페이스로 관리하여 빌드하고, 요소를 추가/제거할 수 있는 기능을 제공하는 등 확장 프로그램을 개발하는 데 유용한 기능을 제공합니다. 만약 신규 프로젝트를 계획하고 계신다면 이 보일러플레이트를 사용해보실 것을 추천드립니다.
세 파트로 구성한 시리즈로 크롬 확장 프로그램에 대해 알아보고, 개발 환경 구성과 간단한 확장 프로그램을 만들어보았습니다. 브라우저의 내부 기능을 통해 다양한 비즈니스 요구사항을 구현할 수 있는 만큼 기술을 응용하는 데 많은 도움이 되었으면 좋겠습니다.
Chrome Extension
Mar 4, 2024
20 min read
크롬, 파이어폭스, 엣지, 오페라와 같은 주요 브라우저는 기능을 확장하거나 수정할 수 있는 확장 프로그램을 개발할 수 있는 환경과 API를 제공합니다. 확장 프로그램은 웹페이지의 UI를 변경하거나, 브라우저 이벤트를 감지하여 필요한 동작을 수행하는 등 다양한 응용이 가능한데요. 이번 글에서는 크롬 브라우저의 확장 프로그램을 개발하기에 앞서 확장 프로그램의 구성에 필요한 파일들과 그 역할에 대해 알아보려고 합니다.
Chrome Extension
May 4, 2024
15 min read
Chrome, Firefox, Edge, Opera 등 주요 브라우저는 기능을 확장하거나 수정할 수 있는 확장 프로그램을 개발할 수 있는 환경과 API를 제공합니다. 확장 프로그램은 웹페이지의 UI를 변경하거나, 브라우저 이벤트를 감지하여 필요한 동작을 수행하는 등 다양한 응용이 가능한데요. 이번 글에서는 크롬 브라우저의 확장 프로그램을 개발하기에 앞서 확장 프로그램에 필요한 파일 구성과 그 역할에 대해 알아보려고 합니다.