본문으로 건너뛰기 Chrome 확장 프로그램 완벽 가이드 — Manifest V3·서비스 워커·메시징·콘텐츠 스크립트·프로덕션 패턴

Chrome 확장 프로그램 완벽 가이드 — Manifest V3·서비스 워커·메시징·콘텐츠 스크립트·프로덕션 패턴

Chrome 확장 프로그램 완벽 가이드 — Manifest V3·서비스 워커·메시징·콘텐츠 스크립트·프로덕션 패턴

이 글의 핵심

이 글은 Chrome 확장 프로그램을 “API 나열”이 아니라 실행 모델과 격리 경계를 기준으로 설명합니다. Manifest V3의 구성 요소, 서비스 워커 수명, 메시지 전달 경로, 콘텐츠 스크립트의 격리, 스토어·운영 관점의 프로덕션 패턴을 한 흐름으로 연결합니다.

이 글의 범위

Chrome 확장 프로그램은 단일 웹앱이 아니라 여러 실행 컨텍스트(서비스 워커, 확장 페이지, 콘텐츠 스크립트, 필요 시 오프스크린 문서)가 chrome.* API와 메시징으로 묶인 분산 시스템에 가깝습니다. Manifest V3(MV3) 는 보안·성능·스토어 정책을 이 모델에 맞추기 위해 백그라운드를 지속 페이지에서 서비스 워커로 바꾸고, 네트워크 조작·원격 코드 등 위험한 패턴을 제한했습니다.

이 문서는 입문용 체크리스트를 넘어 내부 경계(권한, 격리 월드, 메시지 라우팅)와 수명(서비스 워커 종료, 탭별 콘텐츠 스크립트)을 기준으로 설계 판단을 내릴 수 있게 정리합니다. 프로덕션에서는 이 이론이 곧 장애 대응(메시지 유실, 상태 불일치, 스토어 거절 사유)과 직결됩니다.


Manifest V3 아키텍처 심화

MV3의 핵심은 “권한·엔트리·네트워크·자산 노출”을 선언적으로 고정하고, 백그라운드를 이벤트 기반 서비스 워커로 통일하는 것입니다.

구성 요소와 책임

구성 요소실행 환경대표 역할
서비스 워커 (background.service_worker)Worker 글로벌, DOM 없음이벤트 구독, 탭·스토리지·알람, 메시지 허브
확장 페이지 (팝업, 옵션, 전체 탭 페이지, 사이드 패널)확장 origin의 문서, DOM 있음UI, 사용자 설정, 단기 상태 표시
콘텐츠 스크립트웹 페이지에 주입, 격리 월드DOM 읽기/조작, 페이지와의 브리지
오프스크린 문서 (지원 브라우저)숨은 페이지 컨텍스트DOM·캔버스·일부 API가 필요한 백그라운드 작업

manifest.json 은 이들의 스크립트 경로, permissions / host_permissions, content_scripts 매치, web_accessible_resources, declarative_net_request 규칙 등을 한데 묶는 계약서입니다. MV3에서는 host_permissionspermissions를 분리해 “어떤 API”와 “어떤 호스트”를 쓰는지 심사·사용자 이해에 맞게 드러냅니다.

MV2와 달라진 결정적 제약

  • 백그라운드: 영구 백그라운드 페이지가 사라지고 단일 서비스 워커가 이벤트를 받습니다. “항상 켜진 데몬”처럼 무한 루프를 돌리는 설계는 플랫폼과 맞지 않습니다.
  • 원격 코드: 스토어 정책과 플랫폼 제약 하에서 외부에서 가져온 스크립트로 동작을 바꾸는 패턴은 지양됩니다. 기능은 번들에 포함하고, 원격은 데이터(설정, 실험 플래그) 수준으로 제한하는 편이 안전합니다.
  • 네트워크 차단/수정: webRequestBlocking 중심이 아니라 declarativeNetRequest(DNR) 로 선언적 규칙을 쓰는 흐름이 권장됩니다. 세밀한 요청 후킹이 필요한 경우에도 권한·심사·성능을 함께 고려해야 합니다.

action과 UI 엔트리

툴바 아이콘은 MV3에서 action (구 browser_action)으로 통일되는 경우가 많습니다. default_popup 이 있으면 클릭 시 팝업 문서가 뜨고, 없으면 클릭 이벤트만 서비스 워커에서 처리할 수 있습니다. 사이드 패널 등 별도 엔트리는 매니페스트와 권한 조합이 다르므로, “어디에 상태를 둘지” 를 먼저 정한 뒤 엔트리를 고르는 것이 좋습니다.


서비스 워커 수명 주기

MV3 백그라운드는 브라우저가 유휴 상태로 판단하면 종료할 수 있는 서비스 워커입니다. 웹의 서비스 워커와 유사하게 이벤트가 올 때 깨어나고, 작업이 끝나면 다시 잠들 수 있습니다.

시작·이벤트·종료

  1. 등록: 확장이 로드되면 서비스 워커 스크립트가 등록되고, 최초 실행 시 최상위 코드가 한 번 평가됩니다.
  2. 리스너 등록: chrome.runtime.onInstalled, onMessage, alarms.onAlarm, tabs.onUpdated리스너는 최상위에서 동기적으로 연결하는 것이 안전합니다. 비동기 async 최상위에서만 리스너를 달면, 초기 깨어남 타이밍에 따라 이벤트를 놓칠 수 있습니다.
  3. 유휴 종료: 오래 실행을 붙잡아 두지 않으면 브라우저가 워커를 종료합니다. 메모리상 전역 변수는 다음 깨어남에서 초기화된 상태일 수 있습니다.
  4. 상태 영속: 세션 간 유지가 필요한 값은 chrome.storage(로컬/동기/세션 스코프)나 서버 동기화로 옮깁니다. 대용량 캐시IndexedDB(오프스크린·확장 페이지 쪽) 등 설계가 필요합니다.

주기 작업과 장시간 작업

  • chrome.alarms: 주기적 폴링 대신 알람 API로 깨우는 패턴이 일반적입니다. 최소 간격·정확도는 플랫폼 문서를 확인해야 합니다.
  • 비동기 응답: onMessage에서 sendResponse를 비동기로 쓸 경우 문서에 명시된 패턴(예: return true로 비동기 응답 유지)을 지키지 않으면 호출 측에서 응답이 끊긴 것처럼 보입니다.
  • 오프스크린: DOM이 꼭 필요한 작업은 오프스크린 문서로 옮기고, 서비스 워커는 조율만 하는 식으로 역할을 쪼갭니다.

실무에서는 “워커가 살아 있다”는 가정을 하지 않는 것이 첫 번째 원칙입니다. 깨어날 때마다 storage에서 복원 → 리스너는 항상 동일하게 등록하는 구조가 예측 가능합니다.


메시지 전달 메커니즘

확장의 각 부분은 서로 다른 자바스크립트 컨텍스트에 있으므로, 명시적 메시지로만 안전하게 통신합니다.

일회성 메시지

  • chrome.runtime.sendMessage: 보통 서비스 워커 또는 확장 페이지 → 서비스 워커로 보냅니다. 수신은 chrome.runtime.onMessage 입니다.
  • chrome.tabs.sendMessage: 특정 탭의 콘텐츠 스크립트로 보냅니다. 해당 탭에 콘텐츠 스크립트가 주입되어 있고, 매니페스트·scripting 권한이 맞아야 합니다.

페이로드는 구조화 클론 가능한 객체로 제한됩니다. 함수, DOM 노드 등은 전달되지 않습니다. 버전 필드를 넣어 메시지 스키마를 진화시키면 롤백·호환 처리가 쉬워집니다.

포트 기반 연결

chrome.runtime.connect / tabs.connect장수명 채널을 엽니다. 팝업 ↔ 백그라운드처럼 여러 이벤트를 주고받는 UI에 적합합니다. 포트가 끊기면 onDisconnect 로 정리하고, 재연결 전략을 두는 것이 좋습니다.

라우팅 패턴

규모가 커지면 메시지 타입 문자열 + 페이로드 검증(런타임 스키마)을 두어 한 곳에서 분기합니다. 그렇지 않으면 onMessage 핸들러가 비대해지고, 권한이 다른 코드 경로가 섞여 보안 검토가 어려워집니다.

코드로 보는 핵심: 비동기 응답과 포트

서비스 워커에서 onMessage비동기 작업 끝에 sendResponse 를 호출하려면, Chrome 문서가 요구하는 대로 return true 로 “응답이 아직이다”를 알려야 합니다. 그렇지 않으면 채널이 먼저 닫혀 호출부 runtime.sendMessage의 콜백/Promise가 빈 결과를 받을 수 있습니다.

// background.js (service worker)
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type !== 'FETCH_AND_RETURN') {
    return; // 동기 응답 없음
  }
  (async () => {
    try {
      const data = await chrome.storage.local.get('key');
      sendResponse({ ok: true, data });
    } catch (e) {
      sendResponse({ ok: false, error: String(e) });
    }
  })();
  return true; // 비동기 sendResponse 허용
});

장수명 UI(팝업·옵션)와 백그라운드가 스트림처럼 이야기해야 할 때는 포트가 적합합니다. 한쪽이 사라지면 onDisconnect가 호출되므로, 탭이 닫히거나 확장이 리로드된 뒤에도 좀비 리스너가 남지 않게 정리합니다.

// popup.js — 백그라운드와 포트 연결
const port = chrome.runtime.connect({ name: 'ui-sync' });
port.postMessage({ type: 'SUBSCRIBE' });
port.onMessage.addListener((msg) => {
  /* UI 갱신 */
});
port.onDisconnect.addListener(() => {
  /* 재연결 또는 안내 */
});

콘텐츠 스크립트로 보낼 때는 탭 ID가 필요합니다. 백그라운드는 sender.tab?.id로 응답 경로를 알 수 있고, UI에서 특정 탭으로 보낼 때는 chrome.tabs.sendMessage(tabId, payload)를 사용합니다. 주입 여부(해당 URL에 content_scripts가 매치되는지, 또는 scripting.executeScript로 이미 주입했는지)를 전제로 해야 하며, 그렇지 않으면 수신자 없음 오류가 납니다.


콘텐츠 스크립트 격리

콘텐츠 스크립트는 웹 페이지와 같은 DOM 트리를 보되, 자바스크립트 환경은 분리됩니다. 이를 격리된 월드(isolated world) 라고 부릅니다.

격리 월드가 의미하는 것

  • 페이지가 정의한 window.MyApp 같은 전역과 콘텐츠 스크립트의 전역은 다릅니다. 따라서 라이브러리가 페이지에 로드되었다고 해서 콘텐츠 스크립트에서 같은 심볼을 볼 수 없습니다.
  • DOM 조작은 가능합니다. 스크립트가 만든 노드를 읽고, 속성·이벤트를 다룰 수 있습니다. 다만 페이지 스크립트가 기대하는 이벤트 순서와 충돌하지 않도록 설계해야 합니다.

메인 월드 주입이 필요한 경우

페이지 객체와 직접 같은 JS 힙을 공유해야 할 때는 chrome.scripting.executeScriptworld: "MAIN" (문서에 따라 표기 상이) 등 메인 월드 주입을 사용합니다. 이 경로는 페이지 권한으로 실행되는 코드에 가깝기 때문에 신뢰 경계를 엄격히 두고, 최소한의 브리지만 두는 것이 좋습니다.

run_at과 타이밍

document_start, document_end, document_idle 은 주입 시점을 바꿉니다. 광고 차단·스타일 조기 삽입 등은 document_start 가 필요할 수 있고, DOM이 어느 정도 준비된 뒤가 안전한 로직은 document_idle 이 흔합니다. 레이스 컨디션(SPA 전환, 동적 로딩)은 MutationObserver 와 메시징으로 보완하는 경우가 많습니다.

스타일 충돌

콘텐츠 스크립트가 삽입한 스타일은 페이지 CSS와 전역적으로 경쟁합니다. Shadow DOM, 고유한 클래스 접두사, CSS 변수 스코프 등으로 누수와 깨짐을 줄입니다.


프로덕션 확장 패턴

스토어 배포와 장기 운영을 전제로 한 패턴을 정리합니다.

권한 최소화와 단계적 요청

초기 설치 시 optional_permissions 또는 필요 시점에만 host_permissions 를 요청하면 거부율과 리뷰 리스크를 줄일 수 있습니다. 권한 설명 문자열(스토어 제출용)과 인앱 설명을 일치시키는 것이 중요합니다.

오류·로깅·관측

  • 서비스 워커에서 잡히지 않은 예외는 조용히 실패할 수 있습니다. 중앙 try/catch, 구조화 로그, 필요 시 외부 로깅(개인정보·URL 마스킹)을 고려합니다.
  • 콘텐츠 스크립트는 사이트마다 예외가 다르므로 도메인별 가드기능 플래그로 폭을 제한합니다.

성능

  • 대용량 DOM 순회·무거운 리스너는 메인 스레드와 페이지 성능에 악영향을 줍니다. 디바운스·스로틀, IntersectionObserver 등 웹 성능 기법을 그대로 적용합니다.
  • DNR 규칙 수·콘텐츠 스크립트 주입 범위가 넓을수록 브라우저 부담이 커집니다. 매치 패턴을 좁히고, 불필요한 탭에서는 주입하지 않습니다.

보안

  • 메시지 페이로드 검증: 신뢰할 수 없는 출처를 가정하고 타입·길이·필드를 검사합니다.
  • XSS: 콘텐츠 스크립트가 innerHTML에 사용자 입력을 넣으면 확장 컨텍스트에서도 위험합니다. 텍스트 노드·이스케이프·DOMPurify 등 웹과 동일한 위생을 유지합니다.
  • 업데이트: 확장은 자동 업데이트되므로 마이그레이션(onInstalledreason)으로 스토리지 스키마를 버전업합니다.

팀 개발

  • TypeScript로 메시지·스토리지 스키마를 공유하고, 빌드(Vite, Wxt, Plasmo 등)로 매니페스트와 번들을 일치시킵니다.
  • E2E는 실제 브라우저 드라이버로 “확장 로드 → 시나리오”를 검증하는 편이 신뢰도가 높습니다.

내부 동작과 핵심 메커니즘

이 글의 주제는 「Chrome 확장 프로그램 완벽 가이드 — Manifest V3·서비스 워커·메시징·콘텐츠 스크립트·프로덕션 패턴」입니다. 앞선 튜토리얼을 구현·런타임 관점에서 다시 압축합니다. 요청 경로와 상태 전이를 기준으로 “입력이 어디서 검증되고, 핵심 연산이 어디서 일어나며, 부작용(I/O·네트워크·디스크)·동시성이 어디서 터지는가”를 한 장면으로 그리면 장애 분석이 빨라집니다.

처리 파이프라인(개념도)

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]

경계에서의 지연·실패(시퀀스 관점)

sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(프로세스·런타임·게이트웨이)
  participant D as 의존성(외부 API·DB·큐)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)

알고리즘·프로토콜·리소스 관점 체크포인트

  • 불변 조건(Invariant): 각 단계가 만족해야 하는 조건(버퍼 경계, 프로토콜 상태, 트랜잭션 격리, 파일 디스크립터 상한)을 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 동일 입력에 동일 출력이 보장되는 순수 층과, 시간·네트워크·스레드 스케줄에 의해 달라질 수 있는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화/역직렬화, 문자 인코딩, syscall 횟수, 락 경합, GC·할당, 캐시 미스처럼 누적 비용을 의심 목록에 넣습니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때(소켓 버퍼, 큐 깊이, 스트림) 어디서 어떤 신호로 속도를 줄일지 정의합니다.

프로덕션 운영 패턴

실서비스에서는 기능과 함께 관측·배포·보안·비용·규제가 동시에 요구됩니다.

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율/지연 분위수(p95/p99), 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시 계층·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션 호환성·플래그가 문서화되어 있는가
용량피크 트래픽·디스크·파일 디스크립터·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 가능한 한 프로덕션에 가깝게 맞추는 것이 재현율을 높입니다.


확장 예시: 엔드투엔드 미니 시나리오

「Chrome 확장 프로그램 완벽 가이드 — Manifest V3·서비스 워커·메시징·콘텐츠 스크립트·프로덕션 패턴」을 실제 배포·운영 흐름으로 옮긴 체크리스트형 시나리오입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드 표를 API 또는 이벤트 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 한 화면(로그+메트릭+트레이스)에서 추적한다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지(또는 피처 플래그) 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값이 기대 범위인지 본다.

의사코드 스케치(프레임워크 무관)

handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)        // 경계에서 거절
  authorize(validated, ctx)                  // 권한·테넌트
  result = domainCore(validated)             // 순수에 가까운 규칙
  persistOrEmit(result, idempotentKey)       // I/O: 멱등·재시도 정책
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성 불안정, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정이 로컬과 다름프로필·시크릿·기본값, 지역 리전단일 소스(예: 스키마 검증된 설정)와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

정리

MV3는 서비스 워커 중심·선언적 권한·격리된 콘텐츠라는 경계를 분명히 합니다. 상태는 영속 저장소로, 통신은 메시지 계약으로, 페이지와의 상호운용은 격리 월드와 메인 월드를 구분해 설계하면 확장은 단순 스크립트 모음이 아니라 작은 분산 시스템으로 다룰 수 있습니다. 이 관점이 디버깅 시간과 스토어 승인 결과를 가르는 경우가 많습니다.


참고