브라우저 확장프로그램 시리즈 (4/5)
앞의 세 편에서 생태계, 가능성, 구조를 이해했어요. 이제 손으로 만들 차례예요. 처음부터 끝까지 따라 하면서 확장프로그램 하나를 완성합니다. 이 편이 끝나면 여러분은 브라우저에서 돌아가는 자기 확장프로그램을 갖게 될 거예요. 30분이면 됩니다.
들어가며 — 오늘 만들 것
오늘 만들 확장은 “웹페이지 분석기” 예요. 어떤 페이지든 열고 툴바의 확장 아이콘을 클릭하면, 팝업이 뜨면서 이런 정보를 보여줘요.
- 페이지 제목과 URL
- 페이지 내 이미지 개수
- 링크 개수
- 단어 수와 예상 읽기 시간
- 다크 모드 토글 버튼 (누르면 현재 페이지를 어둡게 바꿈)
- 다크 모드 상태 저장 (다른 페이지에서도 설정 유지)
이 하나의 프로젝트에 3편에서 배운 핵심 요소가 전부 들어가요.
- ✅ manifest.json — 확장 설계도
- ✅ Popup — 팝업 UI
- ✅ Content Script — 페이지 분석 및 다크 모드 적용
- ✅ Background Service Worker — 설치 시 초기화 로직
- ✅ Storage — 다크 모드 설정 저장
- ✅ 메시지 패싱 — Popup과 Content Script 간 통신
완성하면 툴바 아이콘을 누를 때 이미지 개수·링크 개수·단어 수·읽기 시간이 뜨는 팝업이 열리고, 버튼 하나로 페이지를 어둡게 바꿀 수 있는 확장이 됩니다.
0단계. 프로젝트 폴더 만들기
원하는 위치에 프로젝트 폴더를 하나 만들어주세요. 저는 my-analyzer로 할게요.
my-analyzer/
├── manifest.json
├── popup.html
├── popup.css
├── popup.js
├── content.js
├── background.js
└── icons/
├── icon16.png
├── icon48.png
└── icon128.png
아이콘은 당장 없어도 돼요. 간단히 정사각형 PNG 파일 세 개를 준비해서 위 이름으로 저장하세요. 없으면 Flaticon, Icons8 같은 사이트에서 무료 아이콘을 받아도 되고, 임시로 빈 PNG를 만들어도 됩니다.
💡 아이콘 없이 시작하려면
manifest.json의icons와action.default_icon부분을 일단 빼고 시작해도 동작은 해요. 나중에 추가하세요.
1단계. manifest.json — 설계도 작성
모든 건 여기서 시작해요. 다음 내용으로 manifest.json을 만드세요.
{
"manifest_version": 3,
"name": "웹페이지 분석기",
"version": "1.0.0",
"description": "현재 웹페이지를 분석하고 다크 모드를 적용합니다",
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png"
}
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_idle"
}
],
"permissions": ["storage", "activeTab", "scripting"]
}
각 부분 해설
manifest_version: 3— 현재 표준action.default_popup— 툴바 아이콘 클릭 시 뜰 HTMLbackground.service_worker— 백그라운드 스크립트 파일content_scripts.matches: ["<all_urls>"]— 모든 URL에 content.js 주입run_at: "document_idle"— 페이지 로딩이 안정화된 후 실행 (분석에 적합)permissions:storage— 설정 저장용activeTab— 현재 탭 접근 (더 강한tabs대신 선택)scripting— 스크립트 주입 API 사용
2단계. popup.html — 팝업 UI 뼈대
다음으로 팝업의 구조를 만들어요. popup.html 파일을 만들고 이 내용을 넣으세요.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="container">
<header>
<h1>🔍 페이지 분석기</h1>
</header>
<section class="info">
<div class="row">
<span class="label">제목</span>
<span class="value" id="pageTitle">-</span>
</div>
<div class="row">
<span class="label">URL</span>
<span class="value" id="pageUrl">-</span>
</div>
</section>
<section class="stats">
<div class="stat-card">
<div class="stat-value" id="imgCount">-</div>
<div class="stat-label">이미지</div>
</div>
<div class="stat-card">
<div class="stat-value" id="linkCount">-</div>
<div class="stat-label">링크</div>
</div>
<div class="stat-card">
<div class="stat-value" id="wordCount">-</div>
<div class="stat-label">단어 수</div>
</div>
<div class="stat-card">
<div class="stat-value" id="readTime">-</div>
<div class="stat-label">읽기 시간</div>
</div>
</section>
<section class="actions">
<button id="darkModeToggle" class="btn">
🌙 다크 모드 켜기
</button>
<button id="refresh" class="btn btn-secondary">
🔄 새로고침
</button>
</section>
<footer>
<span id="status">준비 완료</span>
</footer>
</div>
<script src="popup.js"></script>
</body>
</html>
⚠️ 주의:
<script>태그는 반드시 파일 맨 아래에 놓으세요. 확장프로그램에서는 인라인 스크립트가 금지되어 있어요.onclick="..."같은 속성도 쓸 수 없어요. 모든 이벤트는 JS 파일에서addEventListener로 처리해야 합니다.
3단계. popup.css — 스타일링
팝업을 예쁘게 꾸며볼게요. popup.css 파일을 만드세요.
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 320px;
font-family: -apple-system, "Noto Sans KR", sans-serif;
background: #f7f8fc;
color: #1a1d2e;
}
.container {
padding: 16px;
}
header h1 {
font-size: 16px;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 2px solid #e8ecf5;
}
.info {
background: white;
border-radius: 10px;
padding: 12px;
margin-bottom: 12px;
font-size: 12px;
}
.info .row {
display: flex;
margin-bottom: 6px;
}
.info .row:last-child {
margin-bottom: 0;
}
.info .label {
min-width: 50px;
color: #888;
font-weight: 600;
}
.info .value {
flex: 1;
color: #1a1d2e;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
margin-bottom: 12px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 12px;
border-radius: 10px;
text-align: center;
}
.stat-value {
font-size: 20px;
font-weight: 800;
}
.stat-label {
font-size: 11px;
opacity: 0.9;
margin-top: 2px;
}
.actions {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 10px;
}
.btn {
padding: 10px;
border: none;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
background: #1a1d2e;
color: white;
transition: transform 0.1s;
}
.btn:hover {
transform: translateY(-1px);
}
.btn.btn-secondary {
background: white;
color: #1a1d2e;
border: 1px solid #d8dce8;
}
.btn.active {
background: #f39c12;
}
footer {
font-size: 11px;
color: #888;
text-align: center;
padding-top: 8px;
border-top: 1px dashed #d8dce8;
}
4단계. content.js — 페이지 분석기
이제 진짜 로직이에요. Content Script가 페이지를 분석하고, 팝업이 요청하면 데이터를 반환합니다. content.js를 만드세요.
// content.js
console.log('[페이지 분석기] content script 로드됨');
// 페이지 분석 함수
function analyzePage() {
const images = document.querySelectorAll('img');
const links = document.querySelectorAll('a[href]');
// 본문 텍스트 추출 (스크립트, 스타일 제외)
const bodyText = document.body.innerText || '';
const words = bodyText.trim().split(/\s+/).filter(w => w.length > 0);
const wordCount = words.length;
// 분당 200단어 기준 읽기 시간 계산
const readTime = Math.max(1, Math.round(wordCount / 200));
return {
title: document.title,
url: window.location.href,
imageCount: images.length,
linkCount: links.length,
wordCount,
readTime
};
}
// 다크 모드 적용 함수
function applyDarkMode(enabled) {
const styleId = 'page-analyzer-dark-mode';
const existing = document.getElementById(styleId);
if (enabled) {
if (existing) return;
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
html {
filter: invert(1) hue-rotate(180deg) !important;
background: white !important;
}
img, video, iframe, picture, svg {
filter: invert(1) hue-rotate(180deg) !important;
}
`;
document.documentElement.appendChild(style);
} else {
if (existing) existing.remove();
}
}
// 페이지 로드 시 저장된 다크 모드 설정 적용
chrome.storage.local.get('darkMode').then((result) => {
if (result.darkMode) {
applyDarkMode(true);
}
});
// 팝업에서 오는 메시지 수신
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('[페이지 분석기] 메시지 수신:', message);
if (message.action === 'analyze') {
const data = analyzePage();
sendResponse(data);
}
if (message.action === 'toggleDarkMode') {
applyDarkMode(message.enabled);
sendResponse({ success: true });
}
// 동기 응답이므로 return 값 필요 없음
});
여기서 배울 포인트
document.querySelectorAll로 이미지·링크 개수 세기document.body.innerText로 본문 텍스트만 추출 (HTML 태그 제외)- CSS
filter: invert()로 초간단 다크 모드 구현 chrome.runtime.onMessage로 팝업 메시지 수신chrome.storage.local에서 저장된 설정 읽기
5단계. popup.js — 팝업 로직
팝업이 content script에게 분석을 요청하고, 결과를 화면에 그리는 코드예요.
// popup.js
// 현재 활성 탭 가져오기
async function getCurrentTab() {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
return tab;
}
// 페이지 분석 요청
async function analyzePage() {
const status = document.getElementById('status');
status.textContent = '분석 중...';
try {
const tab = await getCurrentTab();
// 일부 페이지(chrome://, 웹스토어)에서는 content script가 동작하지 않음
if (!tab.url || tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://')) {
status.textContent = '이 페이지에서는 분석할 수 없어요';
return;
}
// content script에 분석 요청
const response = await chrome.tabs.sendMessage(tab.id, { action: 'analyze' });
// 결과를 UI에 반영
document.getElementById('pageTitle').textContent = response.title || '(제목 없음)';
document.getElementById('pageTitle').title = response.title;
document.getElementById('pageUrl').textContent = response.url;
document.getElementById('pageUrl').title = response.url;
document.getElementById('imgCount').textContent = response.imageCount;
document.getElementById('linkCount').textContent = response.linkCount;
document.getElementById('wordCount').textContent = response.wordCount.toLocaleString();
document.getElementById('readTime').textContent = response.readTime + '분';
status.textContent = '분석 완료 ✓';
} catch (error) {
console.error(error);
status.textContent = '분석 실패: content script 로드를 기다려주세요';
}
}
// 다크 모드 토글
async function toggleDarkMode() {
const tab = await getCurrentTab();
const { darkMode = false } = await chrome.storage.local.get('darkMode');
const newState = !darkMode;
// 저장
await chrome.storage.local.set({ darkMode: newState });
// content script에 적용 요청
try {
await chrome.tabs.sendMessage(tab.id, {
action: 'toggleDarkMode',
enabled: newState
});
} catch (error) {
console.error('content script 미응답:', error);
}
// 버튼 UI 업데이트
updateDarkModeButton(newState);
document.getElementById('status').textContent = newState ? '다크 모드 ON' : '다크 모드 OFF';
}
// 다크 모드 버튼 모양 업데이트
function updateDarkModeButton(enabled) {
const btn = document.getElementById('darkModeToggle');
btn.textContent = enabled ? '☀️ 다크 모드 끄기' : '🌙 다크 모드 켜기';
btn.classList.toggle('active', enabled);
}
// 초기화: 팝업이 열릴 때
document.addEventListener('DOMContentLoaded', async () => {
// 다크 모드 현재 상태 반영
const { darkMode = false } = await chrome.storage.local.get('darkMode');
updateDarkModeButton(darkMode);
// 자동으로 페이지 분석
analyzePage();
// 이벤트 연결
document.getElementById('darkModeToggle').addEventListener('click', toggleDarkMode);
document.getElementById('refresh').addEventListener('click', analyzePage);
});
여기서 배울 포인트
chrome.tabs.query로 현재 탭 조회chrome.tabs.sendMessage로 content script에 메시지 전송await로 비동기 처리 깔끔하게 작성chrome://페이지 체크 (에러 방지)chrome.storage.local에 설정 저장·복원
6단계. background.js — 서비스 워커
이번 예제에서는 background가 하는 일이 많지 않아요. 설치 시 초기화 정도만 담당합니다.
// background.js
console.log('[페이지 분석기] 서비스 워커 시작');
// 확장이 처음 설치되거나 업데이트될 때
chrome.runtime.onInstalled.addListener(async (details) => {
console.log('[페이지 분석기] onInstalled:', details.reason);
if (details.reason === 'install') {
// 기본 설정 저장
await chrome.storage.local.set({
darkMode: false,
installedAt: Date.now()
});
console.log('[페이지 분석기] 설치 완료, 기본 설정 저장됨');
}
if (details.reason === 'update') {
const previousVersion = details.previousVersion;
const currentVersion = chrome.runtime.getManifest().version;
console.log(`[페이지 분석기] ${previousVersion} → ${currentVersion} 업데이트`);
}
});
// (선택) 스토리지 변경 감지 — 디버깅·로깅용
chrome.storage.onChanged.addListener((changes, area) => {
console.log(`[페이지 분석기] storage.${area} 변경:`, changes);
});
여기서 배울 포인트
chrome.runtime.onInstalled로 설치·업데이트 이벤트 처리chrome.runtime.getManifest()로 현재 버전 가져오기chrome.storage.onChanged로 모든 스토리지 변화 감지
7단계. 확장프로그램 로드하기
드디어 테스트할 시간이에요!
크롬의 경우:
- 주소창에
chrome://extensions입력 - 우측 상단 개발자 모드 토글 ON
- 압축해제된 확장 프로그램을 로드합니다 버튼 클릭
my-analyzer폴더 선택
엣지의 경우:
- 주소창에
edge://extensions입력 - 좌측 개발자 모드 토글 ON
- 압축 해제된 확장 로드 클릭
- 폴더 선택
로드에 성공하면 확장 카드가 목록에 뜨고, 툴바에 아이콘이 생겨요.
💡 아이콘이 안 보이면
크롬은 기본적으로 확장 아이콘을 퍼즐 아이콘 안에 숨깁니다. 퍼즐 아이콘을 클릭하고 우리 확장 옆의 핀 아이콘을 눌러서 툴바에 고정하세요.
8단계. 테스트 — 동작 확인
아무 웹사이트나 열어보세요. 네이버, 위키피디아, 여러분의 블로그 등이요.
- 툴바의 확장 아이콘 클릭
- 팝업이 뜨면서 자동으로 페이지를 분석
- 이미지 수, 링크 수, 단어 수, 읽기 시간이 표시됨
- 🌙 다크 모드 켜기 버튼 클릭 → 페이지가 어두워짐
- 다른 탭을 열고 같은 사이트 접속 → 다크 모드가 자동 적용되는지 확인 (스토리지 저장이 동작하는 증거)
잘 되시나요? 축하해요! 🎉 여러분은 방금 실제로 동작하는 브라우저 확장프로그램을 만든 거예요.
9단계. 디버깅 — 안 되면 어떻게 하나
확장프로그램은 3군데에서 각각 디버깅해야 해요. 환경이 분리되어 있으니까요.
Popup 디버깅
팝업을 띄운 상태에서 팝업 안에서 우클릭 → “검사”. DevTools가 뜨면 팝업 전용 콘솔이에요.
Content Script 디버깅
웹페이지에서 F12 → DevTools의 일반 콘솔 에 content script 로그가 찍혀요. console.log('[페이지 분석기]') 같은 식으로 접두어를 붙이면 필터링하기 편해요.
Background Service Worker 디버깅
chrome://extensions → 우리 확장 카드 → 서비스 워커 링크 클릭. 별도 DevTools가 뜨면서 background 콘솔을 볼 수 있어요.
흔한 오류들
“Could not establish connection. Receiving end does not exist.”
→ Content script가 아직 로드 안 된 페이지에서 메시지를 보낸 경우. chrome:// 페이지나 방금 로드한 페이지에서 자주 발생. 페이지 새로고침 후 다시 시도.
팝업에서 alert()이 뜨지 않음
→ 팝업에서 alert는 되는데, Background Service Worker에서는 안 됨. chrome.notifications 사용.
코드 수정했는데 반영이 안 됨
→ chrome://extensions에서 새로고침(↻) 버튼 클릭 필요. Content script는 페이지도 새로고침해야 해요.
10단계. 다음으로 — 확장해볼 아이디어
완성된 확장을 시작점으로 해서 이런 기능들을 추가해볼 수 있어요.
난이도: 쉬움
- 분석 항목 추가 (헤딩 개수, 외부 링크 수, 이미지 총 용량 등)
- 다크 모드 강도 조절 슬라이더
- 분석 결과 복사 버튼
난이도: 중간
- 사이트별 다크 모드 화이트리스트/블랙리스트
- 페이지 내 모든 링크를 새 탭으로 여는 버튼
- 특정 키워드 하이라이트 기능
난이도: 도전
- 옵션 페이지 추가 (다크 모드 색상 커스터마이징)
- 단축키 등록 (
Ctrl+Shift+D로 다크 모드 토글) - 페이지 접속 통계 기록 (
storage에 방문 이력 쌓기)
다음 편에서 이 확장을 실제 스토어에 올리는 법을 알려드릴 테니, 지금 만든 상태에서 기능을 좀 더 다듬어보세요. 스토어에 뭘 올릴지는 여러분의 선택이에요.
전체 파일 구조 정리
최종적으로 폴더는 이런 모습일 거예요.
my-analyzer/
├── manifest.json ← 설계도 (6KB)
├── popup.html ← 팝업 UI (2KB)
├── popup.css ← 팝업 스타일 (2KB)
├── popup.js ← 팝업 로직 (3KB)
├── content.js ← 페이지 분석기 (2KB)
├── background.js ← 서비스 워커 (1KB)
└── icons/
├── icon16.png
├── icon48.png
└── icon128.png
깃허브에 올려두면 포트폴리오로도 활용할 수 있어요.
마무리하며
오늘 이런 걸 했어요.
- ✅ manifest.json으로 확장 설계
- ✅ popup.html/css/js로 사용자 UI 구축
- ✅ content.js로 페이지 분석 및 다크 모드 구현
- ✅ background.js로 설치 초기화
- ✅ 메시지 패싱으로 popup과 content 통신
- ✅ chrome.storage로 설정 영구 저장
- ✅ 실제 브라우저에 로드해서 동작 확인
3편에서 배운 추상적인 개념들이 실제 코드에서 어떻게 쓰이는지 다 경험하셨을 거예요. “아, 메시지 패싱이 이렇게 하는 거구나”, “storage가 환경 간 공유 창구구나” 하고요.
이 확장은 지금 당신의 컴퓨터에서만 돌아가고 있어요. 다음 편에서는 이걸 크롬 웹 스토어와 엣지 애드온 스토어에 올려서 전 세계 사람들이 쓸 수 있게 만들어볼 거예요. 심사 과정에서 자주 거절되는 이유, 스크린샷·설명문 작성 팁, 업데이트 전략까지 전부 다룹니다.
시리즈의 마지막 편에서 만나요. 👋