navigator.hardwareConcurrency, WebGL 렌더러, AudioContext, 화면 크기 같은 수십 개의 신호로 자동화를 잡아낸다. camofox-browser는 이 신호들을 JS가 보기도 전에 네이티브 레벨에서 위조하고, 그 위에 에이전트 친화적인 두 가지 혁신 — 접근성 스냅샷(HTML보다 ~90% 작은 페이지 표현)과 엘리먼트 ref(e1, e2 같은 안정적 클릭 식별자) — 을 얹었다.
jo-inc/camofox-browser · 언어 JavaScript/TypeScript · 라이선스 MIT · package.json 버전 1.13.0 · 런타임 Node ≥22 · 핵심 의존성 camoufox-js 0.10.2 + playwright-core 1.58 + express 4 · 최신 커밋 2026-07-20, 해시 8b5b095 · npm 패키지명 @askjo/camofox-browser · 제작사 askjo.ai)
camofox-browser는 "봇 탐지에 걸리지 않는 브라우저를 HTTP API 하나로 조종하게 해주는 서버"다. 개발자가 직접 쓰라고 만든 게 아니라, AI 에이전트(LLM)가 도구로 호출하도록 설계됐다. 에이전트가 POST /tabs로 탭을 열고, GET /snapshot으로 페이지의 "읽을 수 있는 요약"을 받고, POST /click으로 버튼을 누른다. 이 모든 게 사람 눈에는 평범한 Firefox 사용자로 보인다.
보통의 웹 자동화는 로봇이 은행에 들어가는 것과 같다 — 걸음걸이가 뻣뻣하고, 눈이 카메라라 경비원(Cloudflare)이 즉시 알아챈다. Camoufox는 그 로봇에게 진짜 사람 피부와 지문, 자연스러운 관절을 입힌다. camofox-browser는 거기에 무전기를 달아준다 — 밖에 있는 에이전트가 "3번 창구로 가서 저 서류를 집어"라고 지시하면, 변장한 로봇이 그대로 수행하고 "집었다"고 보고한다.
중요한 건 무전 대화가 사진(HTML) 대신 짧은 텍스트 목록(접근성 스냅샷)으로 오간다는 점 — 그래야 에이전트의 토큰(대화 용량)이 폭발하지 않는다.
hardwareConcurrency) 등 수십 개 신호를 조합하면 개인 브라우저가 거의 유일하게 특정된다. 자동화 도구는 이 값들이 "기본값"이거나 "서로 모순"되기 때문에 들킨다.
결과물은 단일 바이너리 CLI(npx @askjo/camofox-browser)이자, Docker 이미지이자, OpenClaw 플러그인이다. 실행하면 기본 9377 포트에서 REST 서버가 뜨고, OpenAPI 문서(/docs)까지 자동 생성된다.
2025~2026년 "AI 에이전트가 웹을 쓴다"가 폭발하면서, 역설적으로 "에이전트가 웹에서 차단당한다"는 문제가 동시에 커졌다. camofox-browser가 뜨는 이유는 이 정확한 통증을 건드리기 때문이다.
기존 접근(예: puppeteer-extra-plugin-stealth)은 JavaScript로 탐지 신호를 덮어쓴다. 문제는 그 덮어쓰는 행위 자체가 새로운 신호가 된다는 것. 예를 들어 navigator.webdriver를 지우면, 지워졌다는 사실이 오히려 자동화의 증거가 된다. Camoufox는 이걸 C++ 소스 레벨에서 바꾼다 — JS가 값을 읽을 때는 이미 위조된 값이 "진짜 그 브라우저의 원래 값"처럼 존재한다. 덮어쓴 흔적(getter 함수 변조 등)이 남지 않는다.
JS 스텔스는 위조 신분증에 스티커를 덧붙이는 것 — 가까이서 보면 스티커 자국이 보인다. Camoufox의 C++ 위조는 아예 위조 신분증을 인쇄 단계부터 다시 찍는 것 — 자국이 없다.
Camoufox 엔진 자체는 이 프로젝트의 것이 아니다("mighty shoulders of Camoufox"라고 README가 명시). camofox-browser의 독창성은 엔진을 LLM 에이전트용으로 감싼 방식에 있다:
e1, e2 같은 안정적 ID를 붙인다. 에이전트는 "복잡한 CSS 셀렉터"를 만들 필요 없이 click e5라고만 하면 된다.@google_search, @youtube_search 등 14개 사이트의 검색 URL을 미리 만들어 둔다. 에이전트가 URL 구조를 몰라도 된다.page.accessibility / ARIA 스냅샷 기능을 활용한다.README가 반복 강조하는 포인트: 유휴 시 메모리 ~40MB. 브라우저를 요청이 올 때만 지연 실행(lazy launch)하고, 놀면 자동 종료(idle shutdown)한다. 에이전트 인프라는 24시간 떠 있어야 하는데, 브라우저는 무거우니까 "필요할 때만 깨우는" 전략이 비용을 좌우한다. 이 운영 감각이 별점 급상승의 실질 이유 중 하나다.
봇 탐지 우회는 합법적 용도(내 계정 자동화, 접근성 테스트, 가격 모니터링)와 회색지대 용도(대량 스크래핑, 이용약관 위반)를 모두 가능케 한다. 프로젝트는 MIT 라이선스지만, 대상 사이트의 robots.txt·이용약관·현지 법(예: 한국 정보통신망법의 자동수집 관련 조항)을 지키는 책임은 사용자에게 있다. 학습 목적으로는 "탐지가 어떻게 작동하고 왜 회피가 가능한가"라는 방어자의 관점에서 보는 게 가장 유익하다.
| 도구 | 탐지 회피 방식 | 에이전트 친화성 | 포지션 |
|---|---|---|---|
| Playwright / Puppeteer | 없음 (기본) | 낮음 (raw DOM) | 범용 자동화 표준 |
| puppeteer-stealth | JS 패치 (흔적 남음) | 낮음 | Chrome 계열 우회 |
| Camoufox (엔진) | C++ 소스 위조 | 중간 (라이브러리) | 탐지 회피 엔진 |
| camofox-browser | Camoufox 상속 | 높음 (스냅샷+ref+매크로) | 에이전트용 REST 서버 |
Node.js ≥22 위에서 도는 순수 ESM(ECMAScript Module) 프로젝트다("type": "module"). 서버 본체는 JavaScript(server.js + lib/*.js)로 쓰였고, OpenClaw 플러그인 진입점만 TypeScript(plugin.ts)로 작성돼 빌드 시 dist/plugin.js로 컴파일된다.
import/export 문법을 쓴다. 예전 방식(CommonJS, require)과 달리 브라우저·Node 양쪽 표준이다. package.json의 "type":"module"이 이 파일 전체를 ESM으로 취급하게 만든다./tabs, /snapshot, /click 같은 엔드포인트를 정의한다. 가장 검증된 Node 웹 프레임워크라 안정성을 택한 것./openapi.json과 /docs 대화형 문서가 여기서 나온다./metrics로 노출한다.VirtualDisplay(가상 디스플레이) 헬퍼도 여기서 온다.firefox 드라이버로 한다. camoufox-js가 Playwright와 호환되는 브라우저를 띄우고, 서버는 Playwright API(page.click, page.accessibility, tracing 등)로 조작한다. "core"만 쓴다 — 즉 Playwright의 자체 브라우저 다운로드는 끄고, Camoufox 바이너리만 쓴다는 게 설치 스크립트의 핵심 트릭이다.Playwright는 자동차를 원격 조종하는 리모컨이다. 보통은 표준 렌트카(순정 Firefox/Chrome)에 꽂는다. camofox-browser는 그 리모컨을 번호판·엔진음·타이어 자국까지 위장한 특수 차량(Camoufox)에 꽂는다. 리모컨 조작법(API)은 똑같지만, 도로 위 CCTV(탐지 시스템)는 특수 차량을 평범한 차로 인식한다.
workers/crash-reporter) — 익명화된 크래시 리포트를 GitHub 이슈로 모으는 서버리스 함수. TypeScript + wrangler.toml.Makefile이 CPU 아키텍처(M1/M2의 aarch64, Intel의 x86_64)를 자동 감지해 바이너리를 빌드 밖에서 미리 받아둔다(재빌드 ~30초). Windows용 build.ps1 PowerShell 스크립트도 별도 제공.railway.toml과 Dockerfile.ci로 원격 CI에서 빌드 타임에 바이너리를 받는 별도 경로.tests/ + 각 플러그인 옆 *.test.js.lib/sentry.js로 표현식만 감싸 넣어 미설정 시 무해하게 통과.| 층 | 기술 | 역할 |
|---|---|---|
| 런타임 | Node.js ≥22 (ESM) | 실행 환경 |
| API | Express 4 + swagger-jsdoc | REST 라우팅 + 문서 자동생성 |
| 관측성 | prom-client + Sentry | 메트릭·에러 추적 |
| 브라우저 엔진 | Camoufox (Firefox 포크, C++ 위조) | 탐지 회피 |
| 브라우저 제어 | camoufox-js + playwright-core | 바이너리 실행 + 원격 조종 |
| 미디어 | yt-dlp | YouTube 자막 추출 |
| 대화형 로그인 | noVNC + Xvfb | 사람 개입 로그인 |
| 배포 | Docker / Fly.io / Railway | 어디든 배포 |
| 서버리스 | Cloudflare Workers | 익명 크래시 수집 |
| 테스트 | Jest (unit/e2e/live) | 품질 보증 |
거대한 페이지(예: 무한 스크롤 피드)의 접근성 트리는 수십만 자가 될 수 있다. LLM에 통째로 넣으면 토큰이 폭발한다. lib/snapshot.js는 이걸 페이지네이션 가능한 창(window)으로 자른다. 실제 로직:
// lib/snapshot.js — 큰 스냅샷을 자르되, 꼬리(페이지네이션 링크)는 항상 보존
const MAX_SNAPSHOT_CHARS = 80000; // 약 20K 토큰
const SNAPSHOT_TAIL_CHARS = 5000; // 마지막 5K는 '다음 페이지' 링크용
function windowSnapshot(yaml, offset = 0) {
const total = yaml.length;
// 작으면 그대로 반환
if (total <= MAX_SNAPSHOT_CHARS)
return { text: yaml, truncated: false, ... };
const tail = yaml.slice(-SNAPSHOT_TAIL_CHARS); // 꼬리 확보
const chunk = yaml.slice(offset, offset + budget); // 본문 조각
// "offset=NNNN 로 다시 부르면 더 보여줌" 마커 + 꼬리를 붙여 반환
return { text: chunk + marker + tail, hasMore, nextOffset };
}
단순히 앞부분만 자르면 페이지 하단의 '다음 페이지'·'더 보기' 링크가 사라져서 에이전트가 갇힌다. 이 코드는 항상 본문 조각 + 항상 꼬리를 함께 준다. 덕분에 에이전트는 어느 조각을 보고 있든 네비게이션 수단을 잃지 않는다. 작은 코드지만 "LLM이 실제로 어디서 막히는가"를 이해한 설계다.
서버는 sessions라는 Map을 중심에 둔다. 구조는 Map<userId, { tabGroups: Map<sessionKey, Set<tab>>, cookies, storage }>. 즉 사용자(userId) → 세션(sessionKey) → 탭의 3단 계층이다. 에이전트 A와 에이전트 B가 같은 서버를 써도 쿠키·로그인 상태가 절대 섞이지 않는다. 멀티테넌트(여러 고객이 한 인스턴스 공유) 운영을 전제로 한 설계다.
lib/를 보면 "브라우저는 언제든 죽는다"는 전제로 짠 흔적이 가득하다. 이게 이 레포에서 가장 배울 게 많은 부분이다:
| 모듈 | 무엇을 방어하나 |
|---|---|
| new-page-recovery.js | 탭 생성 중 브라우저 컨텍스트가 죽으면 세션을 재생성해 재시도 |
| inflight.js | 같은 요청이 동시에 여러 번 오면 하나로 합쳐(coalesce) 중복 작업 방지 |
| browser-errors.js | "죽은 컨텍스트/크래시/타임아웃/탭 파괴"를 분류해 재시도 가능 여부 판단 |
| process-ownership.js | 이 서버가 띄운 브라우저 프로세스만 골라 관리(남의 것 안 건드림) |
| tmp-cleanup.js | 고아가 된 임시 파일·죽은 Firefox 프로필 정리 |
| reporter.js (1,101줄) | 30초 워치독으로 행(hang)·메모리 누수 감시, 익명 크래시 리포트 |
이 복구 코드들은 놀이공원 롤러코스터의 안전요원과 같다. 손님(요청)을 태우는 로직은 짧지만, "안전벨트 고장 시 재점검(new-page-recovery)", "같은 사람이 두 번 줄 서면 정리(inflight)", "고장 유형별 대응 매뉴얼(browser-errors)", "장비 이상 상시 감시(reporter)"가 코드의 절반 이상을 차지한다. 튜토리얼 프로젝트와 실전 프로젝트의 차이가 바로 여기다.
1. POST /tabs {userId, sessionKey, macro:"@google_search", query:"날씨"}
└ auth.js: API 키/루프백 검사
└ macros.js: @google_search → https://google.com/search?q=날씨
└ 세션에 브라우저 없으면 지연 실행(lazy launch) → Camoufox 기동
└ new-page-recovery: 컨텍스트 죽으면 재생성
→ { tabId, url, title }
2. GET /tabs/:id/snapshot?userId=...
└ Playwright 접근성 트리 추출 → YAML 직렬화
└ snapshot.js: 80K자 넘으면 윈도잉
→ { snapshot: "…e1 link 날씨…e5 heading…", refs }
3. POST /tabs/:id/click {ref:"e5"}
└ ref → 실제 엘리먼트 핸들 매핑 → page.click()
→ { ok:true, newSnapshot? }
import해 배선만 한다. 각 관심사(인증·프록시·스냅샷·복구)가 독립 파일이라 테스트·교체가 쉽다. 전형적인 "조립기(composition root)" 패턴.plugins/로 분리돼, 없어도 코어가 돈다. 각 플러그인에 AGENTS.md(에이전트용 설명서)와 *.test.js가 함께 있다.
camofox-browser를 이해하려면 "애초에 어떻게 봇이 탐지되는가"를 알아야 한다. 이건 웹 보안·프라이버시의 핵심 주제다. 배울 것: navigator 속성, WebGL/Canvas 지문, AudioContext 지문, TLS/HTTP2 지문, 마우스 동선·타이핑 리듬 같은 행위 지문. Camoufox가 어떤 값들을 위조하는지 역추적하면, 반대로 탐지 시스템이 무엇을 보는지가 보인다.
이 레포의 진짜 교훈은 "브라우저 자동화"가 아니라 "LLM에게 세계를 어떻게 압축해서 보여줄 것인가"다. 접근성 스냅샷(HTML 대신), ref 시스템(셀렉터 대신), 윈도잉(무한 페이지 대신)은 모두 토큰 예산이라는 제약에서 나온 설계다. 자신만의 LLM 도구를 만들 때 그대로 응용할 수 있는 패턴이다.
new-page-recovery, inflight, browser-errors, reporter의 워치독 — 이 조합은 "외부 프로세스(브라우저)에 의존하는 서버를 어떻게 죽지 않게 만드나"의 교과서다. 크래시 분류, 재시도 판정, 중복 요청 병합, 고아 프로세스 정리는 어떤 백엔드에서도 통용되는 기술이다.
lib/proxy.js(277줄)는 "provider 인터페이스"라는 깔끔한 추상을 보여준다. Decodo·BrightData·generic 같은 프록시 업체마다 다른 인증 방식·세션 회전 규칙을 공통 인터페이스 뒤로 숨긴다. 여러 외부 서비스를 갈아끼워야 하는 코드를 짤 때 참고할 만한 전략 패턴(Strategy Pattern)이다.
// lib/proxy.js — provider 추상화 (주석에 인터페이스가 문서화돼 있음)
{
name: 'decodo', // 업체 이름
canRotateSessions: true, // 세션별 회전 지원?
buildSessionUsername(base, opts), // country/city/session을 username에 인코딩
buildProxyUrl(proxy, config), // 최종 프록시 URL 조립
}
Makefile이 uname -m으로 CPU를 감지해 aarch64/x86_64 바이너리를 나눠 받고, Docker 빌드 밖에서 미리 다운로드해 재빌드를 30초로 줄인 트릭은 실전 CI/CD 최적화의 좋은 예다.
초보자가 흔히 만드는 "동작만 하는" 프로젝트와, 실제로 수많은 사용자·불안정한 외부 의존성을 견디는 프로젝트의 차이를 코드로 체감할 수 있다. 딱 그 격차를 배우기 좋은 교재.
| 항목 | 요구/권장 | 메모 |
|---|---|---|
| Node.js | ≥ 22 | ESM·최신 API 사용, 필수 |
| 디스크 | ~300MB+ | 첫 실행 시 Camoufox 바이너리 다운로드 |
| 메모리 (유휴) | ~40MB | 브라우저 미기동 상태 (지연 실행) |
| 메모리 (활성) | 수백 MB~ | Camoufox(Firefox) 기동 시 급증, 탭 수에 비례 |
| OS | Linux / macOS / Windows | Docker 권장. Windows는 WSL2 백엔드 필요 |
| CPU 아키텍처 | x86_64 / aarch64 | Makefile이 자동 감지 |
| yt-dlp (선택) | pip/brew 설치 | YouTube 자막 빠른 경로용 |
| 프록시 (선택) | residential proxy | GeoIP 기반 로케일/타임존 자동 매칭 |
README가 강조하는 배포 타깃은 라즈베리파이·$5 VPS·공유 인프라다. 즉 "전용 고사양 서버가 필요 없다"가 세일즈 포인트. 다만 동시에 많은 탭을 열면 Firefox 프로세스 메모리가 실제 병목이 된다 — 유휴 40MB는 "브라우저가 자고 있을 때" 이야기다.
시스템 Chrome을 쓰려고 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1을 걸어둔 환경이 흔한데, 이게 켜져 있으면 Camoufox 바이너리 다운로드까지 조용히 건너뛰어 런타임에 서버가 크래시한다. postinstall 스크립트가 자기 실행 동안만 이 변수를 해제해 방어한다. 에어갭 환경이면 CAMOUFOX_EXECUTABLE=/path/to/bin으로 번들을 지정하는 게 정석. docker build를 직접 부르지 말고 반드시 make up을 써야 하는 이유도(바이너리 bind mount) 여기 있다.
npm install && npm start로 서버를 띄운 뒤, curl로 POST /tabs에 @wikipedia_search 매크로를 넣어 탭을 열고, GET /snapshot으로 접근성 스냅샷을 받아본다. 목표: raw HTML과 스냅샷의 크기·가독성 차이를 눈으로 확인한다. /docs의 대화형 문서를 활용하면 더 쉽다.
bot.sannysoft.com이나 fingerprint.com 같은 지문 테스트 사이트를 (a) 일반 Playwright Firefox와 (b) camofox-browser로 각각 열어 스냅샷/스크린샷을 비교한다. 목표: Camoufox가 어떤 항목을 통과시키는지 표로 정리해 "탐지 시스템이 보는 신호 목록"을 만든다.
openclaw plugins install @askjo/camofox-browser로 플러그인을 붙이고, 에이전트에게 "특정 사이트에서 정보를 찾아 요약"을 시켜본다. 목표: camofox_snapshot → camofox_click → camofox_snapshot 루프가 어떻게 에이전트의 "관찰-행동-관찰" 사이클이 되는지 체감한다. (OpenClaw는 트렌딩 상위 오픈소스 에이전트 하네스)
POST /tabs/:id/extract에 JSON Schema를 주고, 스냅샷의 ref를 x-ref로 매핑해 상품명·가격을 구조화된 JSON으로 받아본다. lib/extract.js의 coerceValue(문자열→숫자/불리언 변환)를 읽고, 타입 강제 로직을 직접 확장(예: 배열·날짜 지원)해본다. 목표: "스냅샷 → 스키마 → 정형 데이터" 파이프라인을 이해한다.
lib/macros.js에 새 사이트 매크로(예: @github_search)를 추가하고, lib/browser-errors.js의 에러 분류에 케이스를 하나 더 넣어 new-page-recovery가 재시도하도록 만든다. Jest 테스트(*.test.js)를 함께 작성한다. 목표: "기능 추가"가 아니라 "실패를 어떻게 분류하고 복구하나"라는 프로덕션 사고방식을 손에 익힌다.
page.accessibility.snapshot()과 ARIA 스냅샷navigator 신호들reporter.js를 읽고 워치독이 감지하는 이상 상황 5가지 정리| 용어 | 뜻 |
|---|---|
| Camoufox | 지문 위조를 C++ 레벨에서 하는 Firefox 포크. 이 프로젝트의 엔진 |
| 핑거프린팅 | 쿠키 없이 브라우저를 식별하는 기법 (화면·폰트·GPU·오디오 신호 조합) |
| 헤드리스 브라우저 | GUI 없이 백그라운드로 도는 브라우저. 그 자체가 탐지 신호가 됨 |
| 가상 디스플레이 (Xvfb) | 실제 화면 없이 가짜 디스플레이를 띄워 "헤드리스 아닌 척"하게 하는 도구 |
| 접근성 스냅샷 | 스크린리더용 의미 구조. LLM이 읽기 좋은, HTML보다 ~90% 작은 페이지 표현 |
| 엘리먼트 ref | e1·e2 같은 안정적 요소 ID. 셀렉터 없이 클릭·타이핑 가능 |
| 검색 매크로 | @google_search 등 사이트별 검색 URL을 미리 만든 단축어 |
| 세션 격리 | userId/sessionKey별로 쿠키·스토리지를 완전 분리하는 멀티테넌트 구조 |
| 지연 실행 (lazy launch) | 요청이 올 때만 브라우저를 켜서 유휴 메모리를 아끼는 전략 |
| 윈도잉 (windowing) | 거대한 스냅샷을 머리+꼬리로 잘라 페이지네이션하는 기법 |
| 인플라이트 병합 (coalescing) | 동시에 온 같은 요청을 하나로 합쳐 중복 작업을 막는 것 |
| 워치독 (watchdog) | 주기적으로 상태를 점검해 행·누수를 감지하는 감시 루프 |
| 구조화 추출 (extract) | JSON Schema + x-ref로 스냅샷을 정형 JSON으로 뽑는 기능 |
| OpenClaw 플러그인 | 에이전트 하네스에 도구를 붙이는 확장. 여기선 브라우저 도구 11종 노출 |
| ESM | JavaScript 공식 모듈 시스템 (import/export) |
@askjo/camofox-browser (npx @askjo/camofox-browser로 즉시 실행)