[WebTranslator 개발기 #21] MV3 최소 권한 다이어트, 개인정보처리방침 반려 극복 및 크롬 웹 스토어 정식 출시
앞서 20편에서는 크롬 웹 스토어 정식 출시를 위해 요구되는 5대 필수 요건(개발자 계정, 배포 패키지, 그래픽 에셋, 스토어 등록 정보, 개인정보 보호 관행)을 체계적으로 정리하고, 임시 말풍선 아이콘을 번역 전용 아이콘으로 교체하며 실물 스크린샷 4종을 정돈한 과정을 살펴보았습니다.
스토어 등록에 필요한 외형적 에셋과 메타데이터 준비가 끝났다면, 이제 실제로 구글 심사관의 엄격한 검토를 통과할 수 있도록 확장 프로그램의 내부 구조를 점검하고 최종 배포 패키지를 업로드해야 하는 마지막 단계에 도달했습니다.
크롬 웹 스토어 개발자 대시보드에 확장 프로그램을 등록하는 과정에서 가장 까다로운 장벽 중 하나는 바로 개인정보 보호 관행(Privacy Practices) 탭입니다. 구글은 확장 프로그램이 선언한 권한(Permissions) 하나하나에 대해 엄격한 정당화 사유와 데이터 비수집 인증을 요구하며, 기능에 비해 조금이라도 과도한 권한이 포함되어 있거나 개인정보처리방침 요건을 충족하지 못하면 심사가 반려(Rejected) 처리됩니다.
이번 글에서는 구글의 최소 권한의 원칙(Principle of Least Privilege)에 맞추어 매니페스트에서 불필요한 부가 권한들을 전면 삭제한 권한 다이어트 과정, 번들러 없이 네이티브 ES 모듈을 유지하면서 필요한 파일만 묶어내는 화이트리스트 패키징 스크립트(scripts/package.js)의 구조, 그리고 1차 심사 반려의 원인이었던 개인정보처리방침을 GitHub Gist로 즉시 해결하여 8월 21일 최종 정식 출시를 달성하기까지의 전 과정을 정리하고자 합니다.
1. 구글 심사의 핵심 관문: 최소 권한의 원칙과 과다 권한의 위험
구글 크롬 웹 스토어는 사용자의 개인정보와 브라우징 보안을 보호하기 위해 개발자 프로그램 정책(Developer Program Policies) 내에서 두 가지 핵심 원칙을 강력하게 강제하고 있습니다.
- 단일 목적의 원칙 (Single Purpose Policy): 확장 프로그램은 명확하고 좁게 정의된 하나의 주 기능만을 수행해야 하며, 관련 없는 부가 기능이나 광고 주입 등을 결합해서는 안 됩니다.
- 최소 권한의 원칙 (Use of Permissions / Least Privilege): 확장 프로그램이 명시한 단일 목적을 수행하는 데 필요한 최소한의 데이터와 브라우저 권한만을 요청해야 합니다. '향후 추가될 기능'을 대비해 미리 권한을 선언해 두는 행위는 엄격히 금지됩니다.
문제는 기능 개발을 진행하는 과정에서 편의를 위해 매니페스트에 추가해 두었던 임시 권한들이었습니다. AI 어시스턴트와 대화하며 프로토타입을 확장하다 보면 우클릭 컨텍스트 메뉴, 탭 활성화 감지 등 다양한 가능성을 열어두기 위해 권한들이 무분별하게 누적되곤 합니다.
2. 과감한 권한 다이어트: permissions 1개와 host_permissions의 정당성
지연 없는 심사 통과를 위해, 실제 WebTranslator의 런타임 동작을 면밀히 분석하여 불필요한 권한을 전면 삭제하는 권한 다이어트를 단행했습니다.
| 권한 구분 | 기존 선언 권한 | 최종 정리 결과 | 조치 사유 및 기술적 근거 |
|---|---|---|---|
contextMenus |
선언됨 | 삭제 | 마우스 드래그 기반 인라인 팝업 카드로 UI가 확정되어 우클릭 메뉴를 일절 사용하지 않음 |
scripting |
선언됨 | 삭제 | 동적 코드 주입 대신 content_scripts의 boot.js를 통해 네이티브 ESM을 정적 로드하므로 불필요 |
activeTab |
선언됨 | 삭제 | 글로벌 단축키(Alt+A) 기반 백그라운드 메시징 및 host_permissions로 전역 제어하므로 중복 권한 배제 |
declarativeNetRequest |
선언됨 | 삭제 | 네트워크 헤더 변조 기능이 없으며 백그라운드 Service Worker에서 fetch 통신만 수행하므로 불필요 |
permissions |
- | ["storage"] (유지) |
사용자의 번역 엔진 선택, 커스텀 API 키, 폰트/색상 스타일 설정, 로컬 단어 캐시를 브라우저에 안전하게 저장하기 위한 필수 권한 |
host_permissions |
- | ["<all_urls>"] (유지) |
사용자가 방문하는 모든 웹페이지에서 Alt+A 단축키로 번역을 트리거하고, 사용자 지정 로컬 LLM(Ollama: localhost:11434) 및 클라우드 번역 API와 통신하기 위해 필수적임 |
이로써 manifest.json의 권한 선언부는 오직 permissions: ["storage"] 단 하나와 host_permissions: ["<all_urls>"]만 남긴 간결한 구조로 재편되었습니다.
{
"manifest_version": 3,
"name": "__MSG_appName__",
"version": "1.0.0",
"description": "__MSG_appDescription__",
"default_locale": "ko",
"permissions": [
"storage"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"service_worker": "src/background/index.js",
"type": "module"
},
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"src/content/boot.js"
],
"css": [
"content.css"
],
"run_at": "document_idle"
}
],
"web_accessible_resources": [
{
"resources": [
"src/content/*"
],
"matches": [
"<all_urls>"
]
}
],
"commands": {
"translate-page": {
"suggested_key": {
"default": "Alt+A"
},
"description": "현재 페이지 번역 토글"
}
},
"options_ui": {
"page": "options.html",
"open_in_tab": true
},
"action": {
"default_title": "Web Translator 설정",
"default_popup": "optionPopup.html"
},
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}3. No-Build Native ESM 철학 유지와 화이트리스트 패키징 스크립트
권한 정리가 완료된 후, 크롬 웹 스토어에 업로드할 배포용 압축 파일(.zip)을 생성하는 파이프라인을 점검했습니다.
No-Build 개발 환경과 패키징의 본질
앞서 13편에서 모놀리스 코드를 리팩토링할 당시, Webpack이나 Vite 같은 복잡한 번들러 도입을 배제하고 브라우저 표준 네이티브 ES 모듈(type: module, boot.js의 동적 import())을 직접 실행하는 구조를 확립한 바 있습니다.
이 방식을 통해 개발 중에는 번들러 컴파일이나 트랜스파일 대기 시간 없이, 소스 코드를 수정하고 크롬 확장 프로그램 관리 페이지에서 새로고침(Ctrl+R)만 누르면 즉시 디버깅할 수 있는 쾌적한 개발 루프를 누릴 수 있었습니다.
따라서 본 프로젝트에서 배포 단계의 '빌드'는 코드를 변환·난독화하는 번들러가 아니라, 개발용 문서나 대용량 이미지, 테스트 스크립트를 격리하고 순수 런타임 파일만 골라 담는 화이트리스트 아카이빙(Pure Staging & Packaging)으로 정의됩니다.
scripts/package.js 화이트리스트 자동화 구조
전체 프로젝트 폴더에서 불필요한 파일을 하나씩 지워나가는 블랙리스트 방식은 향후 새로운 문서나 테스트 코드가 추가되었을 때 배포 패키지에 누출될 위험이 큽니다. 따라서 명시된 9개 런타임 항목만 임시 스테이징 폴더로 복사하는 화이트리스트 기반의 scripts/package.js를 구현했습니다.
import fs from "fs";
import path from "path";
import { execSync } from "child_process";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootDir = path.resolve(__dirname, "..");
const distDir = path.resolve(rootDir, "dist");
// 1. manifest.json에서 버전 동적 추출
const manifestPath = path.resolve(rootDir, "manifest.json");
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
const version = manifest.version || "1.0.0";
const zipName = `web-translator-v${version}.zip`;
const zipPath = path.resolve(distDir, zipName);
const tempBuildDir = path.resolve(distDir, "temp_build");
// 2. 화이트리스트 9개 런타임 필수 항목 정의
const includeItems = [
"manifest.json",
"_locales",
"content.css",
"optionPopup.css",
"optionPopup.html",
"options.css",
"options.html",
"icons",
"src"
];
// 3. 임시 스테이징 디렉토리에 재귀 복사 (docs/, images/, .git/ 원천 차단)
// ...
// 4. Windows 내장 PowerShell Compress-Archive로 무의존성 zip 생성
try {
const psCommand = `powershell -Command "Compress-Archive -Path '${tempBuildDir}\\*' -DestinationPath '${zipPath}' -Force"`;
execSync(psCommand, { stdio: "inherit" });
} finally {
// 임시 스테이징 폴더 즉시 삭제
if (fs.existsSync(tempBuildDir)) {
fs.rmSync(tempBuildDir, { recursive: true, force: true });
}
}archiver 같은 외부 npm 모듈을 추가하지 않고, OS 내장 PowerShell Compress-Archive 명령을 활용함으로써 빌드 도구 의존성 관리 비용을 없애고 런타임 파일 44개만을 담은 초경량 배포본(dist/web-translator-v1.0.0.zip)을 완성했습니다.
4. 개발자 대시보드 실전 입력: 영문 소명서 및 개인정보 보호 서약
생성된 dist/web-translator-v1.0.0.zip을 개발자 대시보드에 업로드한 후, 개인정보 보호 관행(Privacy Practices) 탭에서 구글 심사관에게 제출한 공식 영문 소명서와 데이터 정책 서약 내용입니다.
1) 단일 목적 설명 (Single Purpose Description)
Web Translator is designed solely to provide inline web page translation and text-selection dictionary lookup using multiple translation engines (Google Translate, Gemini AI, OpenAI, Claude, and local LLMs).
(국문 요약: Web Translator는 다양한 번역 엔진을 활용하여 웹 페이지 텍스트를 인라인으로 번역하고 선택한 단어의 사전 뜻을 제공하는 단일 목적의 번역 도구입니다.)
2) 호스트 권한 (<all_urls>) 사유서
The extension requires <all_urls> permission to detect and translate text content across websites the user visits upon pressing the translation shortcut (Alt+A), and to communicate with user-configured translation API endpoints (Google Translate, Gemini API, OpenAI API, Anthropic API, and local Ollama server).
(국문 요약: 사용자가 방문하는 모든 웹페이지에서 Alt+A 단축키를 눌렀을 때 텍스트를 즉시 감지하여 번역하고, 사용자가 설정한 로컬/클라우드 API 엔드포인트와 통신하기 위해 필요합니다.)
3) storage 권한 사유서
Used to save user preferences, custom style options, API keys, and local translation dictionary cache securely in the browser.
(국문 요약: 사용자의 번역 엔진 선택, API 키, 스타일 옵션, 로컬 단어 캐시를 브라우저에 안전하게 보관하기 위해 사용됩니다.)
4) 데이터 사용 인증
- 사용자 데이터 수집 여부:
아니요, 사용자 데이터를 수집하거나 사용하지 않습니다선택 - 개발자 프로그램 정책 준수 인증: 데이터 판매 및 비인가 전송 금지 서약 체크 완료
5. 1차 심사 반려와 GitHub Gist를 활용한 즉각 대응
2026년 8월 14일, 모든 준비를 마치고 개발자 대시보드에서 1차 심사 제출을 완료했습니다.
그러나 약 5일간의 검토 끝에 8월 19일, 구글 심사관으로부터 반려(Rejected) 통보를 받게 되었습니다. 원인은 다름 아닌 개인정보처리방침(Privacy Policy) URL 누락이었습니다.
<all_urls>처럼 웹페이지 콘텐츠에 접근하는 호스트 권한을 사용하는 확장 프로그램은 대시보드에 사용자가 언제든 열람할 수 있는 공개 웹 URL 형태의 개인정보처리방침을 반드시 제공해야 합니다. 데이터 비수집 체크만으로는 부족하며 유효한 URL 입력이 필수적이었던 것입니다.
확장 프로그램만을 위한 별도의 독립 웹 서버나 상용 도메인을 즉시 개설하기에는 비용과 시간이 낭비될 수 있었습니다. 이에 대한 가장 깔끔하고 신속한 해법은 GitHub Gist를 활용하는 것이었습니다.
- Gist 문서 작성: 데이터 비수집, API 키 로컬 보관(chrome.storage), 외부 전송 금지 원칙을 명시한 영문/국문 개인정보처리방침 마크다운 문서를 작성.
- Gist 공개 발행: GitHub Gist에 퍼블릭으로 게시하여 영구적인 정식 웹 URL(Privacy Policy Gist 링크)을 확보.
- 대시보드 반영 및 즉시 재제출: 개발자 대시보드 계정 및 스토어 등록정보의 '개인정보처리방침' 항목에 해당 Gist URL을 입력하고 8월 19일 당일 즉시 재제출 완료.
6. 최종 승인: 크롬 웹 스토어 정식 출시와 21편의 대장정
개인정보처리방침 URL을 보완하여 재제출한 결과, 이틀 뒤인 2026년 8월 21일 최종 심사를 통과하여 크롬 웹 스토어에 정식 출시되었습니다.
👉 Chrome Web Store — Web Translator 공식 다운로드 페이지 (확장 프로그램 ID:
hhcbmjlanabbljhkpfppkhkheanfdppo)
| 개발 단계 | 주요 마일스톤 및 다룬 핵심 과제 | 연계 회차 |
|---|---|---|
| 1단계: 통신 & 아키텍처 | MV3 CSP 우회 통신, 단축키 레이스 컨디션 및 스토리지 분리 | 01편 ~ 02편 |
| 2단계: DOM & 렌더링 | Steam 인라인 링크 색상 복원, 버튼 붕괴 방지 및 듀얼 렌더러 구축 | 03편 ~ 05편 |
| 3단계: LLM 다중 엔진 | 선택 영역 번역 상태 격리, JSON 응답 정제, 단어 사전 전문화 프롬프트 | 06편 ~ 09편 |
| 4단계: API 장애 복원력 | Gemini Rate Limit 한도 극복, 모델 404 동적 탐색, 빈 응답 재시도 큐 | 10편 ~ 12편 |
| 5단계: 모듈화 & UI 혁신 | 3,000줄 모놀리스 ESM 리팩토링, 환경 적응형 보색 계산, 툴바 빠른 설정 | 13편 ~ 16편 |
| 6단계: 이미지 번역 R&D | Tesseract 한계, PaddleOCR 메모리 폭증 격리와 v2.0 분리 | 17편 ~ 18편 |
| 7단계: 고도화 & 스토어 출시 | 스마트 드래그 필터링, 정식 에셋 제작, 최소 권한 다이어트 및 스토어 정식 출시 | 19편 ~ 21편 |
프로젝트 기획부터 시작해 21편의 개발기를 거치며 수많은 DOM 레이아웃 붕괴, API 한도 초과, 모델 삭제, 아키텍처 전면 리팩토링, 그리고 이미지 번역의 기술적 한계와 격리까지 수많은 엔지니어링 과제들을 차분하게 해결해 왔습니다.
크롬 웹 스토어 정식 출시로 v1.0.0의 안정적인 배포 기반을 마련한 만큼, 앞으로 안전하게 격리해 둔 이미지 번역 엔진 고도화를 재개하여 v2.0 메이저 업데이트로 다시 찾아뵙겠습니다.
마무리하며
크롬 확장 프로그램을 개발하면서 가장 크게 깨달은 점 중 하나는, 훌륭한 기능 구현만큼이나 스토어 심사 정책에 부합하는 최소 권한 설계와 체계적인 패키징 파이프라인이 프로덕트 출시에 결정적인 역할을 한다는 사실입니다.
이로써 길고 지루했던 AI를 이용한 크롬 웹 스토어 출시가 완료되었습니다. 현재 AI를 이용해 작업하고, 작업 내용을 바탕으로 기존에 작성해두었던 Blog 자동화를 이용해 포스팅까지 완료했습니다.
이 작업을 하면서 블로그 자동화의 로직도 매우 많이 수정이 되었고, 블로그 자동화는 버전을 두 번이나 업데이트 하며 고도화 해나가고 있습니다.
이처럼 AI를 이용해 새로운 무언가를 한다는 것은 매우 즐겁고 재미있는 일이지만, AI가 작성하는 많은 내용이 사실과 다르거나, 제대로 된 결과물을 한 번에 뽑아낼 수 없다는 점에 유의해야 합니다.
혹시 크롬 웹 스토어에 확장 프로그램을 배포하는 과정에서 개인정보처리방침 URL이나 <all_urls> 권한 소명 문제로 심사 반려를 겪으셨던 경험이 있으신가요? 여러분만의 심사 대응 노하우나 권한 관리 팁이 있다면 댓글로 자유롭게 공유해 주시면 감사하겠습니다.