Use Cases (업데이트: 2026. 6. 2.)

Claude Code로 Chrome 확장 개발하기: Manifest V3, 메시징, 최소 권한

Claude Code로 Chrome MV3 확장을 만든다. manifest, service worker, content script, 권한, 저장소, 테스트를 다룬다.

Claude Code로 Chrome 확장 개발하기: Manifest V3, 메시징, 최소 권한

UI보다 확장의 경계를 먼저 정한다

Chrome 확장은 파일 수가 적어 보여도 실제로는 작은 제품에 가깝다. manifest.json, Manifest V3, 백그라운드 service worker, content script, 메시지 전달, 저장소, 권한 설명, 보안 검토, 패키징, Chrome Web Store 제출까지 모두 포함된다. Claude Code에게 “Chrome 확장을 만들어 줘”라고만 말하면 <all_urls>, tabs, popup, React, Vite, 외부 스크립트처럼 처음부터 필요하지 않은 요소가 섞일 수 있다.

이 글에서는 범위를 작게 잡는다. https://example.com/*에서 선택한 텍스트를 우클릭 메뉴로 하이라이트하는 Manifest V3 확장을 만든다. 필요한 파일은 manifest.json, service-worker.js, content-script.js이고, 설정은chrome.storage.local에 저장한다. service worker와 content script는 runtime message로 통신한다. 이렇게 작게 만들면 복사해서 실행하기 쉽고, 권한 설명도 명확하며, Claude Code 리뷰도 구체적으로 받을 수 있다.

용어를 먼저 정리하자. Manifest V3는 현재 Chrome 확장에서 쓰는 주요 manifest 형식이다. service worker는 이벤트가 올 때 깨어나는 백그라운드 처리자이며 항상 실행되는 프로세스가 아니다. content script는 웹 페이지에 주입되어 DOM을 읽거나 수정하는 스크립트다. message passing은 확장 내부의 서로 다른 실행 환경이 JSON 메시지를 주고받는 방식이다. agentic 개발에서 harness라는 말을 보면 “에이전트가 안전하게 일하도록 돕는 발판” 정도로 이해하면 된다.

구현 중에는 공식 문서를 함께 확인한다. Manifest 항목은Chrome Extensions Manifest, service worker 개념은About extension service workers, content script 제약은Chrome Content scriptsMDN Content scripts, 권한 설계는Chrome Declare permissionsMDN permissions를 기준으로 본다. Claude Code 지시문을 다듬고 싶다면 내부 글인Claude Code 생산성 팁도 같이 읽으면 좋다.

실제로 쓸 수 있는 세 가지 사례

첫 번째 사례는 사내 문서 리뷰다. 확장을 https://docs.example.com/*에만 허용하고, 릴리스 노트에서 제품명이나 API 이름을 선택해 관련 단어를 하이라이트한다. 대상 도메인이 좁기 때문에 보안 담당자에게 “이 확장은 문서 사이트만 읽는다”고 설명하기 쉽다.

두 번째 사례는 고객 지원 업무 보조다. 관리자 화면에서 주문 번호나 고객 ID를 선택하고, 우클릭 메뉴로 하이라이트하거나 복사한다. 이 경우 개인정보가 걸릴 수 있으므로 Claude Code에게 “무엇을 읽는지, 무엇을 저장하는지, 무엇을 외부로 보내지 않는지”를 표로 만들게 해야 한다.

세 번째 사례는 콘텐츠와 SEO 검수다. draft 페이지에서 description 길이, h2 개수, 내부 링크, 공식 외부 링크를 검사할 수 있다. CLI 검사는Claude Code CLI 도구 개발 방식으로 처리하고, 브라우저 표시 검사는 확장으로 처리하면 검수 흐름이 분리된다. 보안 관점은Claude Code 보안 베스트 프랙티스와 함께 보는 것이 좋다.

Claude Code에 줄 프롬프트

MV3 확장 프롬프트에는 기능, 금지 사항, 검증 방법을 같이 넣는다. 특히 service worker가 상주하지 않는다는 점과 넓은 권한을 쓰지 않는다는 점을 명시한다.

Manifest V3 Chrome 확장 샘플을 만들어 주세요.

요구 사항:
- 대상 URL은 https://example.com/* 로 제한
- 우클릭 메뉴에서 선택한 텍스트를 하이라이트
- manifest.json, service-worker.js, content-script.js를 순수 JavaScript로 작성
- chrome.storage.local에 enabled와 color 저장
- runtime message로 content script와 통신
- <all_urls>, tabs, eval, 외부 CDN 스크립트, 원격 코드 사용 금지
- 확장이 로드됐는지 확인하는 Playwright smoke script 추가
- Chrome 공식 문서를 기준으로 권한 리뷰 표 작성

핵심은 기능을 줄이는 것이다. Claude Code는 친절하게 popup, options page, 아이콘, 번들러를 붙이려 할 수 있다. 첫 번째 버전에서는 권한, 메시지, DOM 조작, 테스트만 작게 닫아야 한다.

파일 구성과 Manifest

다음 구성은 chrome://extensions에서 Load unpacked로 바로 불러올 수 있다.

mv3-highlighter/
  manifest.json
  service-worker.js
  content-script.js
  package.json
  playwright-extension-smoke.mjs

manifest.json은 확장의 입구다. 이번 예제는 content_scripts.matcheshttps://example.com/*로 제한하고, 필요하지 않은host_permissions를 추가하지 않는다.

{
  "manifest_version": 3,
  "name": "Claude Code MV3 Highlighter",
  "version": "0.1.0",
  "description": "Highlights selected text on example.com from a context menu.",
  "action": {
    "default_title": "MV3 Highlighter"
  },
  "permissions": ["contextMenus", "storage"],
  "background": {
    "service_worker": "service-worker.js",
    "type": "module"
  },
  "content_scripts": [
    {
      "matches": ["https://example.com/*"],
      "js": ["content-script.js"],
      "run_at": "document_idle"
    }
  ]
}
영역설정이유넣지 않은 것
API 권한contextMenus, storage메뉴와 설정 저장만 필요tabs, scripting, downloads
페이지 범위https://example.com/*데모 도메인 하나로 제한<all_urls>
host permissions없음정적 content script match로 충분넓은 host 권한
원격 코드없음MV3와 스토어 심사를 단순화CDN script, eval

service worker 코드

MV3 service worker는 이벤트가 올 때 깨어나고, 처리가 끝나면 멈출 수 있다. 따라서 중요한 설정을 메모리 변수에만 두면 안 된다. 아래 코드는 설정을 storage에서 읽고, 우클릭 메뉴 이벤트가 오면 content script로 메시지를 보낸다.

const MENU_ID = "highlight-selection";
const DEFAULT_SETTINGS = {
  enabled: true,
  color: "#fff176"
};

async function readSettings() {
  return chrome.storage.local.get(DEFAULT_SETTINGS);
}

chrome.runtime.onInstalled.addListener(async () => {
  const settings = await readSettings();
  await chrome.storage.local.set(settings);

  chrome.contextMenus.create({
    id: MENU_ID,
    title: "Highlight selected text",
    contexts: ["selection"],
    documentUrlPatterns: ["https://example.com/*"]
  });
});

chrome.contextMenus.onClicked.addListener(async (info, tab) => {
  if (info.menuItemId !== MENU_ID || !tab?.id || !info.selectionText) {
    return;
  }

  const settings = await readSettings();
  await chrome.tabs.sendMessage(tab.id, {
    type: "HIGHLIGHT_SELECTION",
    text: info.selectionText.slice(0, 120),
    enabled: settings.enabled,
    color: settings.color
  });
});

chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
  if (message?.type === "GET_SETTINGS") {
    readSettings().then(sendResponse);
    return true;
  }

  if (message?.type === "SAVE_SETTINGS") {
    const nextSettings = {
      enabled: Boolean(message.enabled),
      color: String(message.color || DEFAULT_SETTINGS.color)
    };
    chrome.storage.local.set(nextSettings).then(() => {
      sendResponse({ ok: true, settings: nextSettings });
    });
    return true;
  }

  return false;
});

비동기sendResponse를 사용할 때 return true를 빼먹는 것이 흔한 실수다. Promise가 끝나기 전에 메시지 채널이 닫힐 수 있으므로Chrome Message passing을 보며 검토한다.

content script 코드

content script는 페이지의 DOM을 수정하지만, 비밀 정보나 API 키를 보관하는 장소가 아니다. service worker에서 메시지를 받으면 필요한 부분만 수정하고, Playwright가 확인할 수 있도록 data-* 표식을 남긴다.

const HIGHLIGHT_CLASS = "cc-mv3-highlight";

document.documentElement.dataset.ccMv3Highlighter = "ready";

function shouldSkipNode(node) {
  const parent = node.parentElement;
  if (!parent) return true;
  return Boolean(
    parent.closest(
      `script, style, textarea, input, [contenteditable="true"], .${HIGHLIGHT_CLASS}`
    )
  );
}

function clearHighlights() {
  for (const mark of document.querySelectorAll(`.${HIGHLIGHT_CLASS}`)) {
    const text = document.createTextNode(mark.textContent || "");
    mark.replaceWith(text);
    text.parentElement?.normalize();
  }
}

function createMark(text, color) {
  const mark = document.createElement("mark");
  mark.className = HIGHLIGHT_CLASS;
  mark.textContent = text;
  mark.style.backgroundColor = color;
  mark.style.color = "#111";
  mark.style.padding = "0 2px";
  return mark;
}

function highlightText(term, color) {
  const query = term.trim();
  if (!query) return 0;

  clearHighlights();

  const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
    acceptNode(node) {
      if (shouldSkipNode(node)) return NodeFilter.FILTER_REJECT;
      return node.nodeValue.toLowerCase().includes(query.toLowerCase())
        ? NodeFilter.FILTER_ACCEPT
        : NodeFilter.FILTER_SKIP;
    }
  });

  const matches = [];
  while (walker.nextNode()) {
    const node = walker.currentNode;
    const index = node.nodeValue.toLowerCase().indexOf(query.toLowerCase());
    if (index >= 0) matches.push({ node, index });
  }

  for (const { node, index } of matches.reverse()) {
    const selected = node.splitText(index);
    selected.splitText(query.length);
    selected.replaceWith(createMark(selected.nodeValue, color));
  }

  return matches.length;
}

chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
  if (message?.type !== "HIGHLIGHT_SELECTION") {
    return false;
  }

  if (!message.enabled) {
    clearHighlights();
    sendResponse({ ok: true, count: 0 });
    return false;
  }

  const count = highlightText(String(message.text || ""), message.color || "#fff176");
  sendResponse({ ok: true, count });
  return false;
});

이 구현은 의도적으로 작다. 각 text node의 첫 번째 일치만 처리하고, form, script, style, contenteditable 영역은 건드리지 않는다. 실제 제품에서는 반복 일치, Shadow DOM, 동적 콘텐츠, 기존mark와의 충돌을 테스트해야 한다.

Playwright와 수동 확인

완전한 확장 E2E는 복잡하지만, 확장이 로드되고 content script가 대상 페이지에 들어갔는지는 자동 확인할 수 있다. Playwright에서는launchPersistentContext를 사용한다.

{
  "type": "module",
  "scripts": {
    "smoke": "node playwright-extension-smoke.mjs"
  },
  "devDependencies": {
    "playwright": "^1.52.0"
  }
}
import { chromium } from "playwright";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const extensionPath = path.resolve(__dirname);
const userDataDir = path.resolve(__dirname, ".pw-extension-profile");

const context = await chromium.launchPersistentContext(userDataDir, {
  headless: false,
  args: [
    `--disable-extensions-except=${extensionPath}`,
    `--load-extension=${extensionPath}`
  ]
});

const page = await context.newPage();
await page.goto("https://example.com/");
await page.waitForFunction(() => {
  return document.documentElement.dataset.ccMv3Highlighter === "ready";
});

console.log("Extension content script is loaded on https://example.com/");
await page.waitForTimeout(3000);
await context.close();
npm install
npm run smoke

수동 확인은 chrome://extensions에서 Developer mode를 켜고 Load unpacked로 폴더를 선택한다. https://example.com/을 열고 제목 일부를 선택한 뒤 “Highlight selected text” 메뉴를 실행한다. 하이라이트가 보이고 service worker 콘솔에 오류가 없으면 첫 검증은 통과다.

함정, 패키징, 실제 결과

첫 번째 함정은tabs권한을 너무 빨리 넣는 것이다. 이 예제는 context menu 이벤트에서 받은tab.id만 사용하므로tabs권한이 필요 없다. 두 번째는 content script에 비밀 정보를 넣는 것이다. 세 번째는 service worker가 계속 살아 있다고 생각하는 것이다. 네 번째는innerHTML로 DOM을 통째로 바꾸는 것이다. 다섯 번째는 디버깅용<all_urls>를 패키징 전에 빼지 않는 것이다.

스토어 제출 전에는Prepare your Extension을 기준으로 아이콘, 스크린샷, 개인정보 설명, 권한 설명, 짧은 소개문을 준비한다. 압축할 때는node_modules와 Playwright profile을 제외한다.

zip -r mv3-highlighter.zip manifest.json service-worker.js content-script.js
Compress-Archive -Path manifest.json,service-worker.js,content-script.js -DestinationPath mv3-highlighter.zip -Force

Chrome 확장 개발의 수익화 포인트는 단순 코드 생성이 아니라 권한 리뷰, 보안 경계, 테스트, 스토어 설명을 팀의 표준으로 만드는 데 있다. ClaudeCodeLab의Claude Code 교육 및 도입 상담에서는 MV3 설계, 권한 표, 검증 체크리스트, 리뷰 프롬프트를 실제 저장소 기준으로 정리할 수 있다. 혼자 학습한다면제품 라이브러리에서 템플릿과 체크리스트를 살펴보면 된다.

Masa가 이 패턴을 실제로 시도했을 때 가장 먼저 드러난 문제는 코드가 아니라 권한 설명이었다. <all_urls>는 데모가 빠르지만 왜 모든 사이트 접근이 필요한지 설명하기 어렵다. https://example.com/*로 좁히고contextMenusstorage만 남기자 Chrome 확장 관리 화면의 경고가 줄었고, Claude Code의 보안 리뷰도 훨씬 구체적이었다. MV3 확장은 “동작한다”보다 “설명 가능한 권한으로 동작한다”가 더 중요한 완료 기준이다.

#Claude Code #browser extension #Chrome extension #Manifest V3 #JavaScript
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.