AI 에이전트를 위한 브라우저 자동화 컨트롤러
풀스택패밀리 연구소 제작
Playwright 기반의 브라우저 자동화 컨트롤러입니다. CDP(Chrome DevTools Protocol)를 통해 Chrome 브라우저를 HTTP API로 제어할 수 있습니다.
이 프로젝트는 moltbot의 브라우저 제어 모듈(src/browser/)을 분리하여 독립적인 패키지로 재구성한 것입니다.
풀스택패밀리 연구소에서 moltbot의 핵심 브라우저 자동화 기능만 추출하여, 다른 프로젝트에서도 쉽게 사용할 수 있도록 별도 패키징 작업을 수행했습니다.
- moltbot
src/browser/디렉토리의 약 52개 TypeScript 파일 추출 - 내부 의존성(
config,infra,logging,media) 간소화 및 독립화 - 독립 실행 가능한 HTTP API 서버 구성
- 단위 테스트 및 E2E 테스트 작성
이 프로젝트는 AI 에이전트가 웹 브라우저를 자유롭게 제어할 수 있도록 설계되었습니다. 에이전트는 HTTP API를 통해 브라우저를 시작하고, 페이지를 탐색하며, 요소를 클릭하거나 텍스트를 입력할 수 있습니다.
- 브라우저 생명주기 관리: Chrome 시작/중지
- 탭 관리: 열기, 닫기, 포커스, 목록 조회
- 페이지 탐색: URL 이동
- 요소 상호작용: 클릭, 타이핑, 호버, 키 입력
- 페이지 스냅샷: AI 친화적인 텍스트 형식으로 페이지 구조 파악
- 스크린샷: PNG/JPEG 형식 지원
- JavaScript 실행: 페이지 내 스크립트 평가
browser-controller는 **Chrome DevTools Protocol (CDP)**을 통해 브라우저를 제어하는 HTTP API 서버를 제공합니다. Playwright를 브라우저 인터페이스로 사용하여 안정적인 페이지 조작 기능을 구현합니다.
┌────────────────────────────────────────────────────────────────┐
│ AI 에이전트 │
│ (Claude, 사용자 스크립트, 기타 LLM 애플리케이션) │
└────────────────────┬───────────────────────────────────────────┘
│ HTTP API
▼
┌────────────────────────────────────────────────────────────────┐
│ browser-controller 서버 │
│ - HTTP 엔드포인트 제공 (/snapshot, /act, /tabs, ...) │
│ - 프로필별 브라우저 컨텍스트 관리 │
│ - 요청 라우팅 및 디스패칭 │
└────────────────────┬───────────────────────────────────────────┘
│
┌───────────┴───────────┐
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Playwright │ │ Chrome/Chromium │
│ (CDP 클라이언트) │◄──►│ (CDP 서버) │
│ - 페이지 조작 │ │ 포트: 9222 │
│ - 스냅샷 생성 │ └──────────────────┘
│ - 요소 상호작용 │
└──────────────────┘
| 원칙 | 설명 |
|---|---|
| HTTP API 우선 | 언어에 독립적인 인터페이스 제공 |
| 역할 기반 참조 | 접근성 트리를 통한 안정적인 요소 식별 |
| 상태 비저장 | 각 요청은 독립적으로 처리 (상태는 브라우저가 관리) |
| 프로필 격리 | 여러 브라우저 인스턴스 동시 실행 지원 |
AI 에이전트가 웹 페이지를 제어할 때 가장 큰 문제는 DOM의 불안정성입니다. CSS 선택자나 XPath는 페이지 구조가 조금만 변경되어도 깨지기 쉽습니다.
이 프로젝트는 **접근성 트리( Accessibility Tree)**를 기반으로 요소를 식별합니다.
┌─────────────────────────────────────────────────────────────┐
│ 페이지 스냅샷 요청 │
│ GET /snapshot?format=ai │
└────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Playwright 접근성 트리 추출 │
│ page.accessibility.snapshot() │
│ │
│ AXNode { │
│ role: "button", │
│ name: "로그인", │
│ children: [...] │
│ } │
└────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 역할 기반 스냅샷 변환 │
│ - 대화형 요소만 필터링 │
│ - 계층 구조 유지 │
│ - 고유 참조 ID 할당 (e1, e2, e3...) │
└────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ AI 친화적 출력 │
│ │
│ - e1: button "로그인" │
│ - e2: textbox "이메일" │
│ - e3: textbox "비밀번호" [password] │
│ - e4: link "비밀번호 찾기" │
└─────────────────────────────────────────────────────────────┘
참조 ID의 장점:
- 안정성: 페이지 구조가 변경되어도 역할(role)과 이름(name)이 유지되면 유효
- 가독성:
e1,e2같은 짧은 ID로 에이전트가 쉽게 참조 - 재사용: 같은 탭에서 참조가 캐시되어 여러 액션에 사용 가능
1. 에이전트 요청
action="snapshot", profile="clawd", targetId="abc123"
│
2. HTTP 서버 수신
- 요청 파싱
- 프로필 확인
- 라우트 디스패칭
│
3. Playwright 세션 관리
- CDP 연결 확보 (캐시됨)
- 페이지 객체 획득
- 상태 모니터링 시작
│
4. 페이지 조작 실행
- 접근성 트리 추출
- 역할 기반 참조 생성
- 로케이터 변환
│
5. 응답 반환
{
snapshot: "...",
refs: { e1, e2, ... },
targetId: "abc123"
}
browser-controller/
├── 서버 계층
│ ├── HTTP 서버 (Express)
│ ├── 요청 라우팅
│ └── 프로필 관리
│
├── 연결 계층
│ ├── Playwright 세션 관리
│ ├── CDP 연결 캐싱
│ └── Chrome 프로세스 제어
│
├── 페이지 조작 계층
│ ├── 스냅샷 생성 (accessibility → role-based)
│ ├── 요소 상호작용 (click, type, hover...)
│ ├── 상태 추출 (쿠키, 스토리지)
│ └── 네비게이션
│
└── 데이터 표현 계층
├── 역할 기반 참조 (e1, e2, e3...)
├── 스냅샷 포맷 (ai, aria)
└── 로케이터 매핑
| 액션 | 설명 | 예시 |
|---|---|---|
snapshot |
페이지 구조 스냅샷 | AI 친화적 텍스트 변환 |
click |
요소 클릭 | ref="e1" |
type |
텍스트 입력 | ref="e2", text="검색어" |
hover |
마우스 호버 | ref="e3" |
scroll |
페이지 스크롤 | direction="down" |
navigate |
URL 이동 | url="https://..." |
wait |
대기 | condition="load" |
evaluate |
JavaScript 실행 | code="..." |
한 서버에서 여러 브라우저 프로필을 동시에 실행할 수 있습니다:
{
"profiles": {
"default": { "cdpPort": 9222 },
"sandbox": { "cdpPort": 9223 },
"automation": { "cdpPort": 9224 }
}
}각 프로필은 독립적인 Chrome 프로세스와 CDP 포트를 가집니다.
npm install- Node.js >= 18.0.0
- Chrome 또는 Chromium 설치 필요
import { startBrowserServer } from "browser-controller";
const server = await startBrowserServer({
port: 27182,
cdpPort: 9222,
headless: false,
});
console.log(`브라우저 제어 서버 실행 중: ${server.baseUrl}`);const browser = server.createController("clawd");
// 브라우저 시작
await browser.start();
// 탭 열기
const tab = await browser.openTab("https://google.com");
// 페이지 스냅샷 획득
const snapshot = await browser.snapshot({ targetId: tab.targetId });
// 요소 클릭 (ref 기반)
await browser.click("e1", { targetId: tab.targetId });
// 텍스트 입력
await browser.type("e2", "검색어", { targetId: tab.targetId, submit: true });
// 브라우저 종료
await browser.stop();| 메서드 | 경로 | 설명 |
|---|---|---|
| GET | / |
브라우저 상태 조회 |
| POST | /start |
브라우저 시작 |
| POST | /stop |
브라우저 중지 |
| GET | /tabs |
탭 목록 조회 |
| POST | /tabs/open |
새 탭 열기 |
| GET | /snapshot |
페이지 스냅샷 |
| POST | /act |
페이지 액션 실행 |
AI 에이전트가 브라우저를 제어할 때 가장 중요한 것은 페이지의 어떤 요소를 조작할지 식별하는 것입니다. 이 프로젝트는 ref 기반 참조 시스템을 사용합니다.
curl "http://127.0.0.1:27182/snapshot?profile=clawd&targetId=..."스냅샷은 페이지를 AI가 이해하기 쉬운 텍스트 형식으로 반환합니다:
- search [ref=e31]:
- combobox "검색" [active] [ref=e42]
- button "Google 검색" [ref=e69]
- button "I'm Feeling Lucky" [ref=e70]
각 상호작용 가능한 요소에는 [ref=e42]와 같은 참조 ID가 부여됩니다.
curl -X POST "http://127.0.0.1:27182/act?profile=clawd" \
-d '{"kind":"type","ref":"e42","text":"검색어","submit":true}'다음은 에이전트가 구글에서 "바이브 코딩"을 검색하고 결과를 파싱하는 과정입니다.
curl -X POST "http://127.0.0.1:27182/start?profile=clawd"
# 응답: {"ok":true,"profile":"clawd"}curl -X POST "http://127.0.0.1:27182/tabs/open?profile=clawd" \
-H "Content-Type: application/json" \
-d '{"url": "https://www.google.com"}'
# 응답: {"targetId":"8B2E87A6...","title":"","url":"https://www.google.com/"}curl "http://127.0.0.1:27182/snapshot?profile=clawd&targetId=8B2E87A6..."스냅샷 결과에서 검색창을 찾습니다:
- search [ref=e31]:
- combobox "검색" [active] [ref=e42] ← 이것이 검색 입력란!
- button "Google 검색" [ref=e69]
curl -X POST "http://127.0.0.1:27182/act?profile=clawd" \
-H "Content-Type: application/json" \
-d '{
"targetId": "8B2E87A6...",
"kind": "type",
"ref": "e42",
"text": "바이브 코딩",
"submit": true
}'
# 응답: {"ok":true,"targetId":"8B2E87A6..."}검색 결과 페이지의 스냅샷을 분석하여 링크를 추출합니다:
- link "바이브 코딩 나무위키 https://namu.wiki › 바이브 코딩" [ref=e353]:
- /url: https://namu.wiki/w/바이브%20코딩
- link "2년간의 'vibecoding'을 끝내고..." [ref=e381]:
- /url: https://news.hada.io/topic?id=26148
- link "바이브 코딩 설명: 도구 및 가이드" [ref=e414]:
- /url: https://cloud.google.com/discover/what-is-vibe-coding?hl=ko
스냅샷에서 /url: 패턴을 파싱하여 검색 결과 URL을 추출합니다:
| # | 사이트 | URL |
|---|---|---|
| 1 | 나무위키 | https://namu.wiki/w/바이브%20코딩 |
| 2 | GeekNews | https://news.hada.io/topic?id=26148 |
| 3 | Google Cloud | https://cloud.google.com/discover/what-is-vibe-coding?hl=ko |
| 4 | 브런치 | https://brunch.co.kr/@hsy110405/33 |
| 5 | 위키백과 | https://ko.wikipedia.org/wiki/바이브_코딩 |
curl -X POST "http://127.0.0.1:27182/stop?profile=clawd"
# 응답: {"ok":true,"stopped":true,"profile":"clawd"}1. 스냅샷 획득 → 페이지 구조 파악
2. ref 식별 → 조작할 요소 찾기
3. 액션 실행 → click, type, navigate 등
4. 결과 확인 → 새 스냅샷으로 상태 확인
5. 반복 → 목표 달성까지 1-4 반복
- link 요소:
/url:속성에서 URL 추출 - button 요소: 클릭 가능한 버튼
- combobox/textbox: 텍스트 입력 가능
- [active]: 현재 포커스된 요소
- [ref=eN]: 액션에 사용할 참조 ID
# 스냅샷에서 원하는 링크의 ref 확인
# - link "자세히 보기" [ref=e15]
# 해당 ref로 클릭 실행
curl -X POST "http://127.0.0.1:27182/act?profile=clawd" \
-d '{"kind":"click","ref":"e15","targetId":"..."}'| 변수 | 설명 | 기본값 |
|---|---|---|
BROWSER_CDP_PORT |
CDP 포트 | 9222 |
BROWSER_HEADLESS |
헤드리스 모드 | false |
BROWSER_EXECUTABLE_PATH |
Chrome 실행 파일 경로 | 자동 감지 |
BROWSER_SERVER_PORT |
HTTP 서버 포트 | 27182 |
BROWSER_BIND_ADDRESS |
바인드 주소 | 127.0.0.1 |
프로젝트 루트에 browser-controller.json 생성:
{
"cdpPort": 9222,
"headless": false,
"serverPort": 27182,
"bindAddress": "127.0.0.1"
}# 단위 테스트
npm test
# E2E 테스트 (Chrome 필요)
npm run test:e2e- CDP 포트는 기본적으로 localhost에만 바인딩됩니다
- CDP 포트를 외부에 노출하면 브라우저를 완전히 제어할 수 있으므로 주의하세요
- 프로덕션 환경에서는
attachOnly: true옵션을 권장합니다
playwright-core버전은 1.58.0으로 고정되어 있습니다- 다른 버전 사용 시 CDP 프로토콜 호환성 문제가 발생할 수 있습니다
MIT
풀스택패밀리 연구소 | 2026