이 글에서는 React (+ typescript, vite) 기반으로 크롬 확장 프로그램을 만드는 방법에 대해 다룹니다
사이드 프로젝트로 재학중인 방송통신 대학교 학습도우미 확장 프로그램을 개발 하였습니다
확장 프로그램을 만들기 위한 기본 세팅을 기록하기 위해 글을 작성합니다
npm create vite@latest sample-chrome-extension -- --template react-ts
cd sample-chrome-extension
npm install
# Chrome API 타입 정의
npm install @types/chrome --save-dev
# 빌드 및 개발에 필요한 추가 패키지
npm install @crxjs/vite-plugin --save-dev
크롬 확장 프로그램을 만들기 위한 최소한의 추가 설정 파일들 입니다
sample-chrome-extension/
├── public/
│ └── manifest.json # 확장 프로그램 설정
├── src/
│ ├── background/
│ │ └── background.ts # 백그라운드 스크립트
│ ├── content/
│ │ ├── content.ts # 웹페이지 조작 스크립트
│ │ └── content.css # 웹페이지 스타일
│ └── types/
│ └── chrome.d.ts # TypeScript 타입 확장
├── popup.html # 확장 프로그램 실행 시 보여질 샘플 팝업
├── vite.config.ts # 크롬 확장용 빌드 설정 추가가 필요함
manifest.json
background.ts
content.ts
chrome.d.ts
vite.config.ts
public/manifest.json
생성:
{
"manifest_version": 3,
"name": "기본 크롬 확장 프로그램",
"version": "1.0.0",
"description": "React + TypeScript + Vite 기본 확장 프로그램",
"permissions": [
"activeTab",
"tabs"
],
"background": {
"service_worker": "src/background/background.ts",
"type": "module"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["src/content/content.ts"]
}
],
"action": {
"default_title": "기본 크롬 확장 프로그램",
"default_popup": "popup.html"
}
}
확장 프로그램 아이콘을 클릭했을 때 나타나는 팝업 UI를 만들어보겠습니다
먼저 public/popup.html
파일을 생성합니다
(main.tsx는 app.tsx를 렌더링 합니다)
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Chrome Extension Popup</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html
그리고 src/App.tsx
파일을 아래와 같이 수정합니다
import { useState } from 'react'
import './App.css'
function App() {
const [message, setMessage] = useState('')
const sendMessageToPage = () => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const activeTab = tabs[0]
if (activeTab?.id) {
chrome.tabs.sendMessage(activeTab.id, {
action: 'showBanner',
message: message || '크롬 확장 프로그램에서 보낸 메시지!'
}, (response) => {
console.log('Response:', response)
})
}
})
}
return (
<div style={{ width: '300px', padding: '20px' }}>
<h1 style={{ fontSize: '18px', marginBottom: '20px' }}>크롬 확장 프로그램</h1>
<input
type="text"
placeholder="메시지를 입력하세요"
value={message}
onChange={(e) => setMessage(e.target.value)}
style={{
width: '100%',
padding: '8px',
marginBottom: '10px',
borderRadius: '4px',
border: '1px solid #ddd'
}}
/>
<button
onClick={sendMessageToPage}
style={{
width: '100%',
padding: '10px',
backgroundColor: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
현재 페이지에 메시지 전송
</button>
<p style={{ fontSize: '12px', color: '#666', marginTop: '10px' }}>
이 버튼을 클릭하면 현재 활성 탭에 배너가 표시됩니다.
</p>
</div>
)
}
export default App
vite.config.ts
수정:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { crx } from '@crxjs/vite-plugin'
import manifest from './public/manifest.json'
export default defineConfig({
plugins: [
react(),
crx({ manifest }) // 크롬 확장 빌드 플러그인
],
// 크롬 확장은 file:// 프로토콜 사용하므로 상대경로 필요
base: './'
})
npm run build
시 dist 폴더에 크롬 확장 형태로 빌드됨src/background/background.ts
:
// 확장 프로그램 설치 시 실행
chrome.runtime.onInstalled.addListener(() => {
console.log('크롬 확장 프로그램이 설치되었습니다!')
})
// 탭이 업데이트될 때 실행 (페이지 로드 완료 시)
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
// 페이지 로딩 완료 시에만 실행
if (changeInfo.status === 'complete' && tab.url) {
console.log('페이지 로드 완료:', tab.url)
// content script에 메시지 전송
chrome.tabs.sendMessage(tabId, {
action: 'pageLoaded',
url: tab.url
}).catch(() => {
// content script가 아직 로드되지 않은 경우 에러 방지
console.log('content script에 메시지 전송 실패 (정상적인 경우)')
})
}
})
// content script로부터 메시지 수신
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
console.log('content script에서 메시지 받음:', request)
// 간단한 응답 전송
sendResponse({ success: true, timestamp: Date.now() })
})
src/content/content.ts
:
// 콘솔에 메시지 출력 (확장 프로그램이 실행됨을 확인)
console.log('크롬 확장 프로그램이 실행되었습니다!')
// 간단한 알림 배너 추가
function addNotificationBanner() {
const banner = document.createElement('div')
banner.innerHTML = '✅ 크롬 확장 프로그램이 정상적으로 로드되었습니다!'
banner.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: #4CAF50;
color: white;
padding: 10px;
display: flex;
justify-content: space-around;
z-index: 10000;
font-family: Arial, sans-serif;
font-size: 14px;
`
document.body.appendChild(banner)
// 3초 후 자동 제거
setTimeout(() => {
banner.remove()
}, 3000)
}
// background script로부터 메시지 수신
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
console.log('background에서 메시지 받음:', request)
if (request.action === 'pageLoaded') {
addNotificationBanner()
sendResponse({ success: true })
}
})
// 페이지 로드 시 배너 표시
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', addNotificationBanner)
} else {
addNotificationBanner()
}
src/content/content.css
:
/* 기본 스타일 리셋 - 웹사이트와 충돌 방지 */
.extension-element {
box-sizing: border-box !important;
font-family: Arial, sans-serif !important;
}
src/types/chrome.d.ts
:
// 기본적인 Chrome API 타입 확장
declare namespace chrome {
namespace runtime {
interface Port {
name: string
onMessage: chrome.events.Event<(message: any) => void>
postMessage: (message: any) => void
}
}
}
// 메시지 타입 정의
interface ChromeMessage {
action: string
data?: any
}
참고:
@types/chrome
패키지가 기본적인 타입을 제공하므로, 추가 타입이 필요한 경우 작성하면 됩니다
npm run dev
개발 모드에서는 실시간으로 코드 변경을 확인할 수 있습니다.
npm run build
빌드 완료 후 dist
폴더가 생성됩니다.
chrome://extensions/
접속dist
폴더 선택Background Script 디버깅:
chrome://extensions/
→ 해당 확장 프로그램 → "뷰 검사 서비스워커" 클릭Content Script 디버깅:
일반적인 문제들:
필터 문제인지 글이 자꾸 비공개로 바뀌어 댓글에 추가합니다
https://github.com/yhj1024/chrome-extension-sample