[WebTranslator 개발기 #01] 크롬 확장 프로그램 기획

크롬 확장 프로그램(WebTranslator) 개발기 1편: 웹페이지 레이아웃을 해치지 않는 번역기 기획 배경과 Content Script에서 발생한 CSP 보안 차단을 Background Service Worker 중계 아키텍처로 극복한 과정을 정리합니다.
크롬 확장 프로그램 WebTranslator 아키텍처 및 CSP 차단 극복 다이어그램

해외 기술 공식 문서(GitHub, MDN)나 스팀(Steam) 상점의 게임 패치 노트를 읽다 보면, 크롬 브라우저 기본 번역 기능의 한계에 부딪히는 경우가 많습니다. 페이지 전체를 번역하면 기존 CSS 레이아웃이 붕괴되어 버튼 위치가 어긋나거나 텍스트가 겹치는 문제가 빈번하게 발생하며, 어색하게 번역된 문장을 원문과 즉각 대조하기 어렵다는 치명적인 단점이 존재합니다.

이전에는 타인이 만든 번역기를 사용하는 방법 밖에는 없었지만 AI의 발달로 누구나 쉽게 이런 작업을 할 수 있게 되었고, 내심 어떤 것을 AI로 개발해볼까 하는 생각이 있었습니다. 그러던 중 마침 사용하던 크롬 확장 번역기가 과금 유도와 계정 가입 등을 요구하여 크게 불편한 상황이 발생했습니다.

이러한 불편을 근본적으로 해소하고자, "원본 웹페이지의 레이아웃을 100% 보존하면서 원문 아래에 번역문을 자연스럽게 삽입하는 비파괴적(Non-destructive) 번역기"WebTranslator 프로젝트를 기획하게 되었습니다. 이번 글에서는 프로젝트의 초기 요구사항 정의부터 Chrome Manifest V3(MV3) 환경에서 발생한 CSP(Content Security Policy) 통신 차단 오류를 해결하기까지의 과정을 정리해 보고자 합니다.

1. 프로젝트 요구사항 및 핵심 설계 목표

WebTranslator는 단순한 번역기를 넘어 개발자와 게이머가 웹 서핑 흐름을 끊지 않고 원문과 번역문을 동시에 소비할 수 있는 환경을 목표로 설계되었습니다.

구분 요구사항 기술적 구현 목표
레이아웃 보존 기존 DOM 구조를 파괴하지 않는 비파괴적 렌더링 원문 텍스트 노드 하단에 독립된 번역 컨테이너 삽입
접근성 & 조작 단축키 기반의 즉각적인 번역 토글 Alt+A 단축키 입력 시 번역/원문 상태 전환
표준 플랫폼 Chrome 최신 확장 프로그램 규격 준수 Manifest V3(MV3) 기반의 서비스 워커 아키텍처 채택

2. 초기 구현 지시와 CSP 통신 차단 에러

기획된 요구사항을 바탕으로 AI 코딩 어시스턴트에게 초기 Chrome MV3 확장 프로그램의 골격 작성을 지시했습니다.

Alt+A 단축키를 누르면 웹페이지의 텍스트를 수집하여 번역 API로 전달하고, 원문 아래에 번역 결과를 자연스럽게 렌더링하는 Chrome MV3 확장 프로그램의 기본 골격을 작성해라.

지시를 받은 AI는 아래와 같이 content.js 내부에서 직접 번역 서버로 fetch() 비동기 요청을 보내는 코드를 작성해왔습니다.

// content.js: Content Script에서 직접 외부 API fetch 시도
async function fetchTranslation(text) {
  const response = await fetch("https://translation-api.example.com/translate", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ q: text, target: "ko" })
  });
  return response.json();
}

하지만 확장 프로그램을 크롬에 로드하고 GitHub와 Steam 웹페이지에서 실행하자마자 콘솔에 빨간색 에러 로그가 출력되며 번역 요청이 완전히 먹통이 되었습니다.

⚠️ 발생한 에러: Refused to connect to 'https://translation-api.example.com/translate' because it violates the following Content Security Policy directive: "connect-src 'self' ...".

원인 분석: 왜 Content Script의 직접 fetch는 차단되는가?

Chrome Manifest V3 환경에서 Content Script는 웹페이지의 DOM 트리에 직접 접근할 수 있지만, 실행되는 자바스크립트 네트워크 컨텍스트는 방문 중인 웹사이트(Host Page)의 CSP 보안 정책을 그대로 상속받습니다.

  • GitHub, Steam, 금융/공공기관 등 강력한 보안 정책을 가진 웹사이트는 connect-src 지시자를 통해 인가되지 않은 외부 도메인과의 fetch/XHR 통신을 브라우저 레벨에서 원천 차단합니다.
  • 따라서 Content Script 내에서 아무리 확장 프로그램 권한을 설정하더라도 호스트 웹페이지의 CSP 제약을 우회할 수 없습니다.

3. 아키텍처 전면 수정: Background 중계 파이프라인

이 문제를 해결하기 위해 AI에게 MV3의 보안 모델에 부합하는 통신 파이프라인 재설계를 지시했습니다.

Content Script에서 직접 외부 API를 fetch하지 마라. 보안 제약이 없는 Background Service Worker가 외부 API 통신을 전담하게 만들고, Content Script는 chrome.runtime.sendMessage로 텍스트만 넘겨받아 렌더링하도록 통신 파이프라인을 전면 수정해라.

개편된 아키텍처의 데이터 흐름은 다음과 같습니다.

컴포넌트 역할 및 실행 컨텍스트 보안 및 통신 특성
Content Script 웹페이지 DOM 수집 및 번역 결과 렌더링 호스트 페이지 CSP 종속, 외부 직접 통신 불가
Message Passing chrome.runtime.sendMessage 브라우저 내부 IPC를 통한 안전한 데이터 교환
Background Service Worker 외부 번역 API 호출 및 데이터 가공 독립 컨텍스트 실행, host_permissions 기반 CSP 우회 통신 가능

세부 구현 코드

먼저 manifest.json에 Background Service Worker와 필요한 호스트 권한을 명시합니다.

{
  "manifest_version": 3,
  "name": "WebTranslator",
  "version": "1.0.0",
  "background": {
    "service_worker": "src/background/index.js",
    "type": "module"
  },
  "permissions": [
    "storage"
  ],
  "host_permissions": [
    "https://*.googleapis.com/*",
    "https://*/*"
  ]
}

Background Service Worker에서는 비동기 메시지 리스너를 열고 외부 API 호출을 처리합니다.

// src/background/index.js
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === "translateBatch") {
    // 외부 번역 API 비동기 호출 처리
    handleTranslation(request.payload)
      .then(result => sendResponse({ success: true, data: result }))
      .catch(error => sendResponse({ success: false, error: error.message }));

    return true; // 비동기 sendResponse 응답 대기를 위해 true 반환 필수
  }
});

async function handleTranslation(payload) {
  const response = await fetch("https://translation-api.example.com/translate", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(payload)
  });
  return await response.json();
}
💡 팁: chrome.runtime.onMessage.addListener 내부에서 비동기 프로미스를 처리할 때, 이벤트 핸들러가 동기적으로 true를 반환하지 않으면 sendResponse 채널이 닫혀 The message port closed before a response was received 오류가 발생합니다.

Content Script는 이제 단순하게 텍스트를 메시지로 전달하고 회신받은 결과만 DOM에 반영합니다.

// src/content/index.js
async function requestTranslation(texts) {
  return new Promise((resolve, reject) => {
    chrome.runtime.sendMessage(
      { action: "translateBatch", payload: { texts, target: "ko" } },
      (response) => {
        if (chrome.runtime.lastError) {
          return reject(chrome.runtime.lastError);
        }
        if (response && response.success) {
          resolve(response.data);
        } else {
          reject(new Error(response?.error || "번역 요청 실패"));
        }
      }
    );
  });
}

4. 검증 결과 및 핵심 교훈

Background 중계 구조로 전환한 뒤, 엄격한 CSP 정책이 걸려 있던 GitHub와 Steam 상점 페이지에서도 오류 없이 안정적으로 번역 API 응답을 수신하고 비파괴적 렌더링이 동작하는 것을 확인했습니다.

  1. AI 코딩 어시스턴트의 한계 인지: AI에게 단순 웹 애플리케이션 관점으로 지시하면 확장 프로그램의 샌드박스 및 보안 격리 모델을 간과하기 쉽습니다.
  2. 아키텍처의 명확한 사전 정의: MV3 확장 프로그램을 개발할 때는 초기부터 'UI 렌더링(Content Script)'과 '네트워크 통신(Background Service Worker)'의 책임을 분리하여 지시해야 합니다.

마무리하며

통신 파이프라인을 성공적으로 개통하면서 첫 번째 기술적 관문을 통과했습니다. 그러나 실제 브라우징 환경에서 Alt+A 단축키를 연속으로 연타하거나, 네트워크 응답이 지연되는 상황에서 비동기 응답 순서가 뒤엉켜 원문과 번역문이 뒤섞이는 새로운 문제(Race Condition)가 발생했습니다.

다음 글에서는 단축키 연타 시 이전 요청을 안전하게 폐기하는 비동기 취소 상태 머신 구축과 크롬 스토리지 용량 분리 과정을 다루어 보겠습니다.

대화 참여하기