API 통신과 Background Script 설계 - CORS 우회, 통신 아키텍처

Category
Criti AI
Status
Published
Tags
cors
proxy
Chrome Extension
Trouble Shooting
Description
Published
Slug
CORS 문제와의 첫 만남
Chrome 확장 프로그램을 개발하면서 가장 큰 장벽 중 하나는 CORS 문제였습니다.
 

문제 상황

// Content Script에서 직접 API 호출 시도 const analyzeContent = async (content: string) => { try { const response = await fetch('http://localhost:3001/api/analysis/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ content }) }); return await response.json(); } catch (error) { console.error('❌ API 호출 실패:', error); } };
 
오류 메시지
Access to fetch at 'http://localhost:3001/api/analysis/analyze' from origin 'https://news.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

CORS가 발생하는 이유

notion image
Content Script의 제약사항
  • Content Script는 웹페이지 컨텍스트에서 실행
  • 웹페이지의 Same-Origin Policy 적용받음
  • 브라우저가 보안상 다른 도메인으로의 요청 차단
 

Background Script 프록시 패턴 설계

해결: Background Script를 프록시로 활용

Background Script는 확장 프로그램의 독립적인 컨텍스트에서 실행되어 CORS 제한이 없음
notion image

1. Background Script

// src/extension/background/index.ts console.log('Background Service Worker 시작'); // API 프록시 메시지 타입 interface ApiProxyMessage { type: 'API_PROXY' | 'HEALTH_CHECK'; endpoint?: string; url?: string; method?: string; headers?: Record<string, string>; body?: unknown; } interface ApiProxyResponse { success: boolean; data?: unknown; error?: string; status?: number; } // 환경 설정 const API_BASE_URL = 'http://localhost:3001'; // 개발 환경 const API_ENDPOINTS = { ANALYZE: '/api/analysis/analyze', HEALTH: '/health', QUICK_CHECK: '/api/analysis/quick-check', ANALYTICS: '/api/analytics/track' }; // 메시지 리스너 설정 chrome.runtime.onMessage.addListener( (request: ApiProxyMessage, sender, sendResponse: (response: ApiProxyResponse) => void) => { console.log('Background Script 메시지 수신:', request.type); // 헬스 체크 if (request.type === 'HEALTH_CHECK') { handleHealthCheck(sendResponse); return true; // 비동기 응답 } // API 프록시 if (request.type === 'API_PROXY') { handleApiProxy(request, sendResponse); return true; // 비동기 응답 } sendResponse({ success: false, error: '알 수 없는 요청 타입' }); return true; } ); // 헬스 체크 핸들러 const handleHealthCheck = async (sendResponse: (response: ApiProxyResponse) => void) => { try { console.log('🔍 서버 헬스 체크 시작'); const response = await fetch(`${API_BASE_URL}${API_ENDPOINTS.HEALTH}`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); if (response.ok) { const data = await response.json(); console.log('헬스 체크 성공:', data); sendResponse({ success: true, status: response.status, data }); } else { console.log('헬스 체크 실패:', response.status, response.statusText); sendResponse({ success: false, status: response.status, error: `서버 오류: ${response.statusText}` }); } } catch (error) { console.error('헬스 체크 네트워크 오류:', error); sendResponse({ success: false, error: `연결 실패: ${error instanceof Error ? error.message : '알 수 없는 오류'}` }); } }; // API 프록시 핸들러 const handleApiProxy = async ( request: ApiProxyMessage, sendResponse: (response: ApiProxyResponse) => void ) => { try { if (!request.url || !request.method) { sendResponse({ success: false, error: 'URL 또는 Method가 없습니다' }); return; } console.log(`API 프록시 요청: ${request.method} ${request.url}`); const fetchOptions: RequestInit = { method: request.method, headers: { 'Content-Type': 'application/json', ...request.headers } }; if (request.body && ['POST', 'PUT', 'PATCH'].includes(request.method)) { fetchOptions.body = JSON.stringify(request.body); } const response = await fetch(request.url, fetchOptions); const responseData = await response.json(); if (response.ok) { console.log('API 프록시 성공:', response.status); sendResponse({ success: true, status: response.status, data: responseData }); } else { console.log('API 프록시 실패:', response.status, responseData); sendResponse({ success: false, status: response.status, error: responseData.error || `HTTP ${response.status}` }); } } catch (error) { console.error('API 프록시 네트워크 오류:', error); sendResponse({ success: false, error: `네트워크 오류: ${error instanceof Error ? error.message : '알 수 없는 오류'}` }); } }; // 확장 프로그램 설치 시 chrome.runtime.onInstalled.addListener(() => { console.log('Criti AI 확장 프로그램 설치 완료'); }); // 액션 버튼 클릭 시 (툴바 아이콘) chrome.action.onClicked.addListener(async (tab) => { if (tab.id) { console.log('액션 버튼 클릭, 사이드바 토글 요청'); try { await chrome.tabs.sendMessage(tab.id, { action: 'toggleSidebar' }); } catch (error) { console.log('Content Script와 통신 실패:', error); } } });
 

메시지 기반 통신 아키텍처

통신 플로우

notion image