Chrome DevTools Protocol로 AI에게 로그인된 브라우저를 맡기다 — Browser Relay 아키텍처와 통합 검증기
왜 로그인된 브라우저가 필요한가
AI 에이전트에게 웹 자동화를 시키려면 보통 Puppeteer나 Playwright로 새 브라우저 인스턴스를 띄웁니다. 문제는 증권사 사이트처럼 로그인 세션이 필수인 서비스입니다. 매번 로그인 플로우를 자동화하는 건 OTP, 공인인증, 캡차 등 인증 장벽 때문에 현실적이지 않습니다.
해결 아이디어는 단순합니다. 이미 로그인된 Chrome 브라우저 탭을 AI가 그대로 제어하면 됩니다. Chrome DevTools Protocol(CDP)과 Socket.io를 조합한 Browser Relay 아키텍처로 이 문제를 풀었습니다.
전체 아키텍처
[AI 에이전트 (MCP 도구)]
│
▼
[OpenClaude 서버 - Browser Relay API]
│ Socket.io (/browser namespace)
▼
[Chrome Extension (background.js)]
│ chrome.debugger API (CDP)
▼
[로그인된 Chrome 탭]
핵심은 Chrome Extension이 CDP 명령의 릴레이 역할을 한다는 점입니다. AI 에이전트가 MCP 도구로 browser_navigate, browser_screenshot, browser_click 같은 명령을 호출하면, 서버가 Socket.io로 Extension에 전달하고, Extension이 chrome.debugger API를 통해 실제 탭에서 CDP 명령을 실행합니다.
Phase별 팀 에이전트 분담 개발
Claude Code의 팀 에이전트 기능을 활용해 Phase별로 작업을 분담했습니다.
| Phase | 담당 | 주요 산출물 |
|---|---|---|
| Phase 4 | Extension 에이전트 | manifest.json, background.js, popup.html |
| Phase 7 | Orchestrator 에이전트 | 통합 검증, 버그 수정, 빌드 확인 |
Chrome Extension 구조
Manifest V3 기반으로, 필요한 권한은 debugger, activeTab, tabs, storage, alarms입니다.
{
"manifest_version": 3,
"name": "OpenClaude Browser Relay",
"permissions": ["debugger", "activeTab", "tabs", "storage", "alarms"],
"background": {
"service_worker": "background.js"
}
}
background.js(Service Worker)가 핵심 로직을 담당합니다. Socket.io 클라이언트로 서버와 연결하고, 서버에서 CDP 명령이 오면 chrome.debugger.sendCommand로 실행한 뒤 결과를 돌려보냅니다.
// background.js - CDP 명령 릴레이 핵심 로직
socket.on('cdp:execute', async ({ tabId, method, params, requestId }) => {
try {
const target = { tabId };
await chrome.debugger.attach(target, '1.3');
const result = await chrome.debugger.sendCommand(target, method, params);
socket.emit('cdp:result', { requestId, result });
} catch (error) {
socket.emit('cdp:result', { requestId, error: error.message });
}
});
Popup UI에서는 서버 URL과 JWT 토큰을 설정하고, 연결 상태를 확인하며, 탭별로 CDP 제어를 활성화/비활성화할 수 있습니다.
통합 검증에서 발견한 실전 버그
Phase 7 통합 검증 단계에서 전체 코드를 병렬로 리뷰하면서 필드명 불일치 버그를 발견했습니다. 이 버그는 단위 테스트만으로는 잡기 어려운, E2E 흐름에서만 드러나는 유형입니다.
버그: 스크린샷 API의 data vs screenshot 필드명 불일치
Browser Relay API는 스크린샷 결과를 data 필드로 반환합니다.
// browser-relay.ts (API 서버)
const screenshot = await executeCdp(tabId, 'Page.captureScreenshot', { format: 'png' });
return { data: screenshot.data }; // "data" 필드
MCP 서버의 browser_screenshot 도구는 응답에서 screenshot 필드를 참조하고 있었습니다.
// 수정 전 (버그)
const response = await callBrowserAPI('screenshot', { tabId });
return { content: [{ type: 'image', data: response.screenshot }] };
// ^^^^^^^^^^ 존재하지 않는 필드
response.screenshot은 undefined가 되어 스크린샷이 항상 빈 값으로 반환됩니다. 수정은 간단합니다.
// 수정 후
const response = await callBrowserAPI('screenshot', { tabId });
return { content: [{ type: 'image', data: response.data }] };
// ^^^^ 올바른 필드
추가 발견: 도구의 네비게이션 헬퍼 불일치
통합 검증 과정에서 파싱 도구들이 executeCdp() 헬퍼를 직접 호출하는 대신 다른 경로를 사용하는 불일치도 함께 발견하고 수정했습니다. 이런 류의 버그는 개별 모듈이 독립적으로 개발될 때 자주 발생합니다.
팀 에이전트 협업에서 얻은 교훈
팀 에이전트 방식의 가장 큰 장점은 Phase별 병렬 개발 속도입니다. Extension 에이전트가 Chrome Extension을 만드는 동안, 다른 에이전트는 서버 측 API를 구현할 수 있습니다.
반면 통합 검증 Phase가 반드시 필요하다는 점도 확인했습니다. 서로 다른 에이전트가 만든 코드 사이의 **인터페이스 계약(필드명, 타입, 엔드포인트 경로)**이 어긋나는 건 피하기 어렵습니다. Orchestrator 에이전트가 전체 파일을 병렬로 읽으면서 불일치를 잡아내는 과정이 핵심이었습니다.
핵심 정리
Browser Relay 아키텍처를 도입할 때 체크리스트:
- Chrome Extension은 Manifest V3 +
debugger권한이 필수 - Service Worker의 생명주기를 고려해
alarms기반 keepalive 구현 필요 - Socket.io 네임스페이스(
/browser)로 일반 채팅과 브라우저 제어 트래픽을 분리 - JWT 토큰으로 Extension ↔ 서버 간 인증
팀 에이전트 통합 검증 체크리스트:
- API 응답 필드명이 호출 측 참조 필드명과 일치하는지 확인
npm run build성공 여부로 타입 레벨 불일치 검출- E2E 흐름(MCP → API → Extension → CDP → 결과 반환)을 순서대로 추적
- 공용 헬퍼 함수가 있다면 모든 모듈이 동일한 헬퍼를 사용하는지 확인