ego-browser와 Aside는 다르게 실패한다
AI 에이전트에 장바구니 조작이나 대화 목록 정리를 맡기려면, 로그인된 페이지를 읽고 버튼을 누른 뒤 실제로 무엇이 바뀌었는지 확인할 수 있어야 합니다. 이 작업을 연결하는 것이 브라우저 자동화 도구입니다. 어떤 도구를 붙이느냐에 따라 에이전트가 조작하는 탭, 사용자가 중간에 개입하는 방식, 실패를 확인하고 변경을 되돌리는 방법이 달라집니다.
ego-browser와 Aside는 모두 에이전트가 브라우저를 관찰하고 조작하도록 연결하는 도구지만, 작업 공간을 다루는 방식이 다릅니다. ego-browser는 사용자 창과 분리된 task space에 에이전트의 탭을 두고, Aside는 사용자 브라우저의 실제 탭을 다룹니다. Aside에는 개별 조작을 호출하는 방식 외에 자연어로 작업 전체를 맡기는 exec도 있습니다. 같은 일을 맡길 수 있어도 사용자와 에이전트가 브라우저를 나눠 쓰는 방식과 호출자가 직접 처리할 범위가 다릅니다.
둘을 비교할 이유는 이 차이가 실제 작업에서 어떤 제약으로 나타나는지 확인하는 데 있습니다. 사용자가 브라우저를 함께 쓰는 동안 작업을 맡길 수 있는지, 조작 실패를 알아챌 수 있는지, 작업이 끝나면 바꾼 데이터와 탭을 정리할 수 있는지가 선택 기준입니다. 쿠팡 장바구니, 팝업과 canvas 조작, 대화 아카이브·원복 미션과 통제 페이지의 동작 시험으로 이 기준들을 살펴봤습니다.
그 과정에서 드러난 차이 하나가 실패를 알리는 방식입니다. ego-browser는 화면 밖 요소를 클릭하지 못했거나 다른 요소를 클릭하고도 성공을 반환했습니다. Aside에서는 TypeError와 SyntaxError가 작업을 막았습니다. 콜드런에서 낭비한 라운드 수는 6대6으로 같았지만, 문제를 찾아 수정하는 과정은 달랐습니다. 속도 우열은 실험의 순서 효과 때문에 판단하지 못했습니다. 이 글은 측정한 동작과 실패 양상을 바탕으로, 두 도구를 사용할 때 어떤 제약을 감수해야 하는지 정리합니다.
이 글은 ego-browser(ego lite) 0.4.7.4와 Aside CLI 1.26.902.1732를 실제 사이트 미션으로 비교한 기록입니다. 환경은 macOS 15.7.9 arm64, Chromium 150입니다.
구조가 다릅니다
먼저 작업 공간과 제어 방식을 비교합니다. ego는 에이전트의 탭을 사용자와 분리하고, Aside는 사용자의 실제 탭을 씁니다.
| ego-browser | Aside | |
|---|---|---|
| 진입점 | ego-browser nodejs <<'EOF' |
aside repl / aside exec / MCP 툴 3종 |
| 격리 | task space — 사용자 창과 분리, 로그인만 상속 | 없음. openTab한 탭이 곧 사용자 실제 탭 |
| 소유권 | agent/agentDelegatedToUser/user 3상태 + 핸드오프 프로토콜 |
통합 소유권·핸드오프 모델 없음 |
| 상태 | 스크립트 지역 변수는 호출마다 재선언, 브라우저 상태는 task space에 보존 | REPL 스코프 유지 (변수명 충돌 주의) |
| 페이지 JS | js(문자열) = CDP Runtime.evaluate |
Playwright API (부분적) |
| 원시 접근 | cdp(method, params) |
없음 |
| 런타임 제약 | Node API 사용 가능 | 샌드박스 — import/require 금지 |
| 사이트 지식 | site-skills 런타임 존재 (연결 문제 있음) | 빌트인 스킬 51종 |
사용자가 브라우저를 같이 쓰고 있는 상황이라면 이 한 줄이 사실상 결론입니다. ego는 사용자 창을 건드리지 않고, Aside는 건드릴 수밖에 없습니다.
관찰 API
에이전트가 페이지를 "보는" 방식도 다릅니다.
ego snapshotText() → 들여쓰기 트리 + [ref=N, loc=..., url=...]
ref는 최신 스냅샷에서만 유효 (CDP backendNodeId 기반)
Aside snapshot(page) → { tree, diff }
ref는 e1 형태, page.locator('e1')로 지연 평가
annotatedScreenshot()이 ref를 이미지에 오버레이Aside의 diff가 반환량을 줄여주는데, 조건부입니다.
| 변경 유형 | tree | diff | 절감 |
|---|---|---|---|
| 페이지 이동 | 56,069자 | 56,069자 | 0% |
| 페이지 내 변경 | 50,818자 | 5,529자 | 89.1% |
이 89.1%는 "매 행동 후 전체 스냅샷"이라는 전제 위의 수치입니다. 같은 판단을 타겟 쿼리 하나로 할 수 있다면 반환량은 더 줄어듭니다. 동일 과업에서 타겟 쿼리와 diff를 직접 비교하지는 않았으니 절감 배율이나 일반적 우위를 주장하지는 않겠습니다.
ego의 결함
click()이 actionability를 검사하지 않습니다
로컬 통제 페이지에 프로브를 깔고 측정했습니다.
| 조건 | ego click(sel) |
Aside page.click(sel) |
|---|---|---|
| 화면 밖 요소 | 성공 반환 · 클릭 안 됨 · 스크롤 안 함 | 자동 스크롤 후 클릭 |
| 0×0 요소 (inline) | 성공 반환 · 클릭 안 됨 | Timeout waiting for element to be ready |
| 화면 안 일반 요소 | 정상, isTrusted: true |
정상 |
| 화면 안 disabled 요소 | 검사 통과 후 isTrusted: false 이벤트 발생 |
거부 |
click(selector)이 resolved element의 actionability를 확인하지 않고 계산된 뷰포트 좌표로 입력을 보냅니다. 그 좌표가 화면 밖이면 무동작하고, 다른 요소가 그 지점을 점유하면 그 요소를 클릭하고도 성공을 반환합니다.
클릭 전 elementFromPoint: overlay
클릭 결과 로그: [{"id":"overlay","isTrusted":true}]"0×0이면 클릭되지 않는다"는 과일반화입니다. position: fixed인 0×0 요소는 elementFromPoint의 hit target으로 남아 정상 클릭됩니다. 반대로 가시 요소의 중앙을 overlay로 덮으면 overlay를 클릭하고 성공을 반환하는데, 이쪽이 더 위험합니다. 무동작은 알아챌 수 있지만 엉뚱한 컨트롤이 부작용을 내는 건 호출자가 알 방법이 없습니다.
근본 원인은 elementCenter('#btn')이 {x:124.4, y:3065.5}를 반환하면서 스크롤하지 않는다는 데 있습니다. 뷰포트 높이는 977입니다. waitForElement()도 화면 밖 요소에 true를 줍니다. 좌표 계산과 입력 프리미티브는 있는데 이를 안전하게 묶는 계층이 없습니다.
실제로 쿠팡 장바구니 삭제 버튼을 처리하다 이 결함 때문에 라운드를 많이 썼습니다. 쓰려면 클릭 전에 스크롤·크기·히트테스트를 직접 확인하는 래퍼를 씌워야 합니다.
async function safeClick(sel, opts) {
const S = JSON.stringify(sel)
await js(`document.querySelector(${S})?.scrollIntoView({block:'center'})`)
await wait(0.3)
const st = await js(`(() => {
const e = document.querySelector(${S}); if (!e) return 'no-element'
const r = e.getBoundingClientRect()
if (r.width === 0 || r.height === 0) return 'zero-size'
if (r.top < 0 || r.bottom > innerHeight) return 'offscreen'
const hit = document.elementFromPoint(r.left + r.width/2, r.top + r.height/2)
return (hit === e || e.contains(hit)) ? 'ok' : 'occluded'
})()`)
if (st !== 'ok') throw new Error('click precheck failed: ' + st)
await click(sel, opts)
}CSS 셀렉터 전용이라 @N ref에는 통하지 않습니다. 그리고 uploadFile에는 이 검사를 넣으면 안 됩니다. 숨겨진 0×0 <input type="file">은 흔한 구현이고 현재 정상 동작합니다.
captureScreenshot()이 어떤 상태에서 응답하지 않습니다
canvas 미션 중과 그 직후 프로브에서 다섯 번 연속 실패했습니다. 매번 ~15초 타임아웃입니다.
try1: ERR 15004ms — CDP request timed out: Page.captureScreenshot
try2: ERR 15004ms — CDP request timed out: Page.captureScreenshot
try3: ERR 15005ms — CDP request timed out: Page.captureScreenshotabout:blank에서도, example.com에서도, 로컬 페이지에서도 같았습니다. 그런데 같은 탭에서 바로 이어 호출한 pageInfo()는 1초 안에 응답했고 js·click·hover·snapshotText·waitForElement도 전부 정상이었습니다. CDP 채널은 살아 있고 이 명령만 응답하지 않았습니다.
창 상태를 바꿔가며 다시 쟀습니다. 앱을 한 번 활성화한 뒤로는 전면·배경·최소화·task space 정리 후까지 다섯 번 다 43~70ms에 성공했고, 실패 상태는 다시 만들어지지 않았습니다. 그래서 0.4.7.4에서 확인된 것은 여기까지입니다. 이 헬퍼는 어떤 상태에서 5/5로 실패하고 다른 상태에서 5/5로 성공하며, 두 상태를 가르는 조건은 특정되지 않았습니다. 포커스만으로는 설명되지 않습니다. 배경과 최소화에서도 성공했으니까요.
실무적으로는 남습니다. 에이전트가 브라우저를 쓸 때 창은 대개 가려져 있거나 뒤에 있습니다. snapshotText()가 접근성 트리를 주니 DOM으로 표현된 UI에는 충분하지만, 마크업이 아니라 그려진 것 — <canvas>·WebGL·이미지·시각적 레이아웃 판단 — 에는 스크린샷이 ego가 노출하는 유일한 일반 관측 경로입니다. canvas 미션에서 첫 수가 captureScreenshot()이었고, 두 번 실패하며 32.4초를 쓴 뒤 canvas.toDataURL('image/png')로 픽셀을 직접 받아 우회했습니다. 그 우회는 대상이 canvas였기 때문에 가능했고, 일반 화면이었다면 탈출구가 없습니다.
site-skills 런타임이 번들 learnings를 못 읽습니다
ego.helpers에 헬퍼가 52개 있고 SKILL.md는 그중 일부만 문서화합니다. elementCenter, iframeTarget, snapshotRaw, 그리고 site-skills 런타임 전체가 문서에 없습니다. 조회 API만 부르면 빈 결과만 보이는데, runSiteTool()까지 부르면 런타임이 원인을 알려줍니다.
Error: site skill not found: "github"
searched: /skills/ego-browser/learnings
EGO_BROWSER_AGENT_WORKSPACE: unset기본 CLI 런타임의 작업 루트가 /라서 존재하지 않는 경로를 뒤집니다. 환경변수를 설치된 스킬 디렉터리로 지정하면 로드와 dispatch는 동작하는데, 배선만으로 기능이 복구되지는 않습니다. 연결 후 번들 GitHub 도구를 실행하면 search_repos는 결과가 있는데도 []를, get_repo_stats는 forks만 채우고 나머지를 빈 문자열로 반환합니다. 번들 셀렉터가 현재 DOM과 맞지 않습니다.
확인된 ego 결함 셋은 모두 이미 상류 이슈 트래커에 올라와 있습니다.
Aside의 결함
repl --help가 없는 함수를 예시로 싣습니다
$ aside repl --help
Notes:
... call openTab(url) or getTabs() first.
Examples:
aside repl --account u1 "const tabs = await getTabs(); console.log(tabs.length)"getTabs는 존재하지 않습니다.
$ aside repl "const tabs = await getTabs(); console.log(tabs.length)"
ReferenceError: getTabs is not defined실제 이름은 listBrowserTabs입니다. 문서화된 예시를 그대로 복사하면 실패합니다. 더 큰 문제는 빠진 쪽입니다. 전역 스코프에는 annotatedScreenshot, attachBrowserTab, installPageScript, snapshot, sleep 같은 것들이 실제로 있는데, --help가 언급하는 건 openTab 하나와 존재하지 않는 getTabs뿐입니다. Aside를 Aside답게 쓰게 해주는 함수들이 어디에도 안내되지 않습니다.
Playwright 표면이 부분적입니다
동작하는 것과 안 하는 것이 섞여 있습니다.
- 동작:
locator().evaluateAll(),filter({hasText}),waitForLoadState(),page.pdf(),page.close() - 미동작:
setViewportSize()(실제 브라우저 탭이라 뷰포트 제어 불가),page.waitForTimeout(),getTabs/listTabs
어느 것이 있고 어느 것이 없는지 미리 알 방법이 없습니다. 뒤에 나오는 실데이터 미션에서도 초기 3라운드가 여기에 들어갔습니다.
영속 스코프와 탭 고아화
REPL 스코프가 호출 사이에 유지되기 때문에 const 재선언이 SyntaxError를 냅니다. 문제는 실패한 호출도 변수명을 소모한다는 겁니다. 에러로 죽은 호출의 선언이 스코프에 등록되어, 재시도하면 원래 에러 대신 SyntaxError가 납니다.
탭 쪽에서는 세션 경계를 넘으면 소유권이 사라집니다. 3회 재현했습니다.
| 시도 | 반환 | 실제 |
|---|---|---|
repl 자기 탭 |
정상 | 닫힘 |
repl attach 후 closeTab |
"no current open tabs" | 안 닫힘 |
repl attach 후 page.close() |
"closed: …" | 안 닫힘 |
exec로 요청 |
"사용자가 연 탭이라 권한 없음" | 안 닫힘 |
사용자가 수동으로 닫아야 합니다. 대조적으로 ego는 completeTaskSpace(id, {keep:false}) 한 번에 정리되고, 사용자가 GUI로 제어권을 가져간 경우에는 하드 스톱으로 명시 거부합니다.
REPL 전역이 저장된 자격증명을 평문 노출합니다
활성 프로필에서 특정 전역 객체를 통째로 직렬화하면 저장된 OAuth 자격증명이 평문으로 함께 나옵니다. 결제 설정, 통신 설정, 권한 규칙도 같이 나옵니다. credentials.json 파일 자체는 -rw-------로 보호돼 있는데 런타임 전역이 그 보호를 우회합니다.
공격 시도가 아니라 "어떤 이미지 모델이 있나" 확인하다 나왔습니다. Object.keys() 대신 값을 출력한 것뿐입니다. 재현 명령은 싣지 않습니다. 노출된 refresh 토큰은 폐기하는 편이 안전하고, 벤더 측 수정 방향은 자격증명 필드 제거 또는 직렬화 시 토큰 패턴 마스킹입니다.
다르게 실패합니다
여기가 두 도구의 성격을 가장 잘 드러내는 지점입니다.
| ego | Aside | |
|---|---|---|
| 낭비 라운드 원인 | 셀렉터·타이밍·거짓 성공 | 자기 도구 API·문법 |
| 성격 | DOM과 싸움 | 자기 도구 API와 싸움 |
| 실패 방식 | 조용함 (성공을 반환) | 시끄러움 (에러를 던짐) |
콜드런 기준 낭비 라운드 총수는 6대6 동률이었습니다. 셀렉터 문제는 양쪽 다 겪었고, 갈린 건 나머지입니다. Aside 쪽은 자기 도구의 API와 문법이었고, ego 쪽은 타이밍과 거짓 성공이었습니다.
디버깅 난이도는 조용히 실패하는 쪽이 높습니다. 에러는 에이전트가 읽고 다음 수를 바꿀 수 있지만, 거짓 성공은 다음 단계까지 진행한 뒤에야 드러납니다.
뒷정리를 확인할 수 있는가
에이전트가 브라우저를 쓰고 나면 뒷정리가 남습니다. 여기서 구조 차이가 한 번 더 드러났습니다. 탭 2개를 열어 둔 채 호출을 끝내고, 완전히 새 호출에서 조회해봤습니다.
### 1) 한 호출에서 탭 2개 열고 닫지 않은 채 종료
연 탭 수: 2
### 2) 완전히 별도의 새 호출에서 잔여 탭 조회
잔여 탭: []aside repl "<코드>"는 호출마다 브라우저 세션이 새로 서고, 프로세스가 끝나면 openTab으로 연 탭이 사라집니다. 정리를 했든 안 했든 빈 배열이 나옵니다. "정리했는가"를 확인할 방법이 없다는 뜻입니다.
ego는 반대입니다. task space는 프로세스가 끝나도 남습니다. 정찰 때 만든 task space 두 개가 그대로 남아 있는 것을 나중에 발견해 손으로 닫았습니다.
| 자기가 연 탭 | 사용자 탭 | |
|---|---|---|
| ego (task space) | 명시적으로 닫아야 함 → 확인에 정보가 있음 | task space 밖이라 건드리지 않음 |
Aside (repl) |
프로세스 종료 시 자동 소멸 → 확인에 정보가 없음 | 남는 것이 정상. 닫으면 오히려 침범 |
이건 보고의 검증력 차이지 위생의 차이가 아닙니다. Aside는 애초에 잔여물이 생기지 않는 구조고, ego는 잔여물이 생길 수 있는 대신 명시적으로 닫아야 합니다.
위임이 프리미티브보다 낫지는 않았습니다
Aside에만 있는 층이 하나 더 있습니다. repl로 Playwright 스타일 JS를 직접 넣는 대신, exec에 자연어로 통째로 맡길 수 있습니다. ego에는 이 층이 없고 작업이 전부 호출자 턴에서 일어납니다.
같은 팝업 미션을 두 층에 각각 넣었습니다. 등급은 무승부입니다. exec는 67초에 전 술어를 충족하고 요구한 JSON 형식까지 지켰고, repl 트랙도 같은 등급입니다.
다만 쓴 도구가 달랐습니다.
repl 트랙 |
exec 트랙 |
|
|---|---|---|
| 관측 | page.evaluate, locator.innerText |
snapshot(page) → ref 트리 |
| 팝업 획득 | page.waitForEvent('popup') |
listBrowserTabs() + attachBrowserTab() |
| 조작 | page.locator(css) |
page.locator('e4') — snapshot이 준 ref |
repl 트랙이 잡은 popup 객체는 일반 Playwright Page와 API 표면이 달라 waitForLoadState·waitForTimeout이 없었고 TypeError가 두 번 났습니다. exec 트랙은 애초에 그 경로를 안 탔습니다.
그런데 snapshot·attachBrowserTab·sleep은 repl 스코프에도 전부 있습니다.
$ aside repl "console.log(['snapshot','attachBrowserTab','sleep'].map(n => n+'='+typeof globalThis[n]).join(', '))"
snapshot=function, attachBrowserTab=function, sleep=function못 쓴 게 아니라 있는 줄 몰랐던 겁니다. exec 에이전트는 내장 스킬 문서를 읽고 시작하지만 repl 사용자에게는 그 문서가 오지 않습니다. 같은 엔진 위에서 위임 층은 문서를 받고 프리미티브 층은 못 받습니다. 능력 차이가 아니라 발견 가능성의 비대칭이고, 고치기도 쉽습니다.
모델 조건이 고정되지 않았다는 점은 짚어둡니다. exec는 --effort medium으로 지정했고 프리미티브 쪽은 기본 모델이었습니다. 두 트랙의 모델 급이 같다는 보장이 없으니, exec가 오류 원인을 즉각 진단한 것이 구조 덕인지 모델 덕인지는 이 실험으로 가를 수 없습니다.
알고 써야 할 것이 하나 더 있습니다. Aside의 기본 모델 설정이 provider: claude-code인 경우 그 에이전트 추론은 사용자 Claude 구독에서 나갑니다. OCR·요약·기억 정리를 담당하는 side-car 층도 같은 경로를 탑니다. 출하 기본값은 다르므로 설정으로 되돌릴 수 있습니다.
능력 시험 — 대부분 무승부였습니다
효율 비교가 결론이 안 나서(아래) 질문을 "할 수 있는가"로 바꿔 네 가지를 더 돌렸습니다. 성공·실패와 차단 코드로만 채점했습니다.
| 미션 | 시험하는 것 | ego | Aside |
|---|---|---|---|
| 별도 창 제어 | 실제 팝업에서 조작하고 부모 창에 반영 | 5 | 5 |
| canvas 좌표 | DOM이 아닌 그림 속 대상을 한 번에 지목 | 5 | 5 |
| 실데이터 상태 변경 | 지정 대화 3건 아카이브 후 원복 | 5 | 2 |
| 원격 실행 | SSH 세션에서 로컬 로그인 브라우저 사용 | 가능 | 가능 |
등급은 5(완주+정리+개입 0)에서 0(도달 실패)까지이고, 별도로 F(거짓 성공)를 뒀습니다.
갈릴 거라고 본 축이 셋 다 축이 아니었습니다. 팝업은 소유권 모델이 다르니 갈릴 줄 알았는데 둘 다 팝업을 열고 검색하고 항목을 고르고 부모 창 입력란에 값이 반영된 것까지 확인했습니다. ego는 TCP를 안 열고 $TMPDIR 하위 유닉스 소켓을 쓰니 SSH 세션에서 못 붙을 줄 알았는데, macOS의 TMPDIR은 세션이 아니라 사용자별이라 둘 다 ssh localhost 세션에서 정상 동작했습니다. canvas 좌표도 둘 다 한 번의 클릭으로 맞혔고 좌표 변환도 각자 정확했습니다.
갈린 건 사용자 실데이터 미션 하나
ChatGPT 대화 3건을 아카이브하고 되돌리는 과제입니다. 유일하게 실제 데이터를 변경하기 때문에 대상을 3개로 고정하고 ID를 명세에 박았습니다.
| ego | Aside | |
|---|---|---|
| 아카이브 | 3건 | 3건 |
| 원복 | 3건 | 0건 |
| 지정 외 대화 변경 | 없음 (증거 제출) | 없음 (증거 제출) |
| 판단 라운드 | 26 (상한 20 초과) | 20 (상한 도달) |
Aside 쪽은 대화 3건을 아카이브한 채 종료했고, 이 사실을 숨기지 않고 "수동 원복이 필요하다"고 적었습니다.
막힌 데가 셋이었습니다. page.click으로 대화 옵션 메뉴 버튼을 누르면 메뉴가 즉시 닫히고 대신 대화 링크가 눌려 엉뚱한 곳으로 이동했습니다. 합성 pointerdown을 직접 디스패치해야 열렸습니다. page.focus·waitForTimeout·keyboard가 없어 초기 3라운드를 API 탐색에 썼습니다. 그리고 결정적으로, 아카이브 목록 다이얼로그를 보고 "행에 대화 ID가 없다"고 잘못 진단해 제목을 확보하는 우회에 라운드를 쏟았는데, 같은 다이얼로그를 다시 조회하니 ID는 a[href^="/c/"]로 그대로 잡혔습니다.
원인은 도구의 한계가 아니라 관측 실패입니다. "Aside로는 이 과제가 불가능하다"는 결론은 이 데이터로 낼 수 없습니다. 앞의 둘은 Aside의 실제 결함이지만 셋째는 arm의 판단 착오입니다.
설계자 잘못도 있습니다. 라운드 상한과 원복 의무가 정면으로 충돌하는데 명세에 우선순위를 적지 않았습니다. ego는 상한을 어기고 원복을 확인했고, Aside는 상한을 지키고 데이터를 변경된 채 남겼습니다. 둘 다 지시를 따른 결과라 이 등급 차이는 할인해서 읽어야 합니다.
한 가지는 양쪽이 똑같이 걸렸습니다. 아카이브 목록의 갱신 지연 때문에 둘 다 20라운드에서 "아직 아카이브돼 있다"고 오판했습니다. 실제로는 해제된 뒤였습니다. 3건을 연속으로 누르면 1건만 반영되고, 호출을 나눠 한 건씩 눌러야 했습니다.
효율은 결론이 나지 않았습니다
쿠팡 미션을 각 2회씩 4런 돌렸습니다. 네 런 다 성공했고 차단·캡차는 0회입니다. 그런데 사전 등록한 순서 효과 검정이 발동해서 우열을 말할 수 없게 됐습니다.
| 런 | 위치 | 라운드 | 낭비 |
|---|---|---|---|
| ego 콜드 | first | 25 | 6 |
| ego 웜 | second | 8 | 0 |
| Aside 콜드 | second | 18 | 6 |
| Aside 웜 | first | 12 | 1 |
ABBA 순서 교대를 쓴 탓에 각 arm의 웜런이 콜드런과 반대 위치에 놓였고, 위치 효과와 학습 효과가 얽혔습니다. arm 내 위치별 차이가 ego 17라운드로 기준선 2라운드를 크게 넘어 INCONCLUSIVE입니다. 셀당 N=1이라 분리할 수 없고, 해소하려면 웜런이 총 8회 필요한데 현재 2회입니다. 그래서 라운드·시간·바이트로 우열을 말하지 않습니다.
계측 과정에서 걸러낸 함정 셋만 적어둡니다. 도구가 보고하는 시간을 그대로 믿으면 안 됩니다. 한 arm이 18라운드 중 13라운드를 duration_ms: 0으로 남겨 "6.7배 빠르다"는 수치가 나왔는데, 실측을 강제하니 오히려 느렸습니다. 총합 바이트로 비교해도 안 됩니다. 한 라운드의 실패(querySelectorAll('*'))가 1,082,464 바이트로 전체의 99%를 차지했고, 제외하면 방향이 뒤집힙니다. 차단 판정은 시계열로 해야 합니다. domcontentloaded 직후 한 시점만 보면 Akamai의 사후 차단을 놓치고, URL·title로 판정해도 안 됩니다. 쿠팡은 차단할 때도 정상 title을 반환하는 경우가 있습니다.
그래서 무엇을 고를 것인가
| 상황 | 선택 | 이유 |
|---|---|---|
| 기본 | ego-browser + 별도 서브에이전트 | 격리·소유권 모델이 명확 |
| 사용자가 브라우저를 병행 사용 중 | ego | 사용자 탭을 건드리지 않음 |
| 중간에 사람 개입이 필요 | ego | 핸드오프가 프로토콜로 정의됨 |
| 외부 npm 모듈 필요 | ego | Aside REPL은 샌드박스 |
| CDP 원시 기능 | ego | cdp() |
| 사용자 실데이터를 되돌려야 하는 작업 | ego | 원복 경로가 짧고 정리 상태를 확인할 수 있음 |
| 화면 밖 요소 클릭 | ego + 우회 필수, 또는 Aside | ego는 조용히 실패 |
| 그려진 화면 관측 (canvas·이미지·레이아웃) | Aside | ego는 captureScreenshot이 상태를 탐 |
| Slack·Gmail·Notion 같은 잡무 | Aside | 상당수 탭 없이 API 경로 |
기본값을 하나만 정한다면 ego-browser에 별도 서브에이전트를 붙이는 쪽입니다. 격리와 소유권 모델이 명확하고, 이번 계정·이번 미션에서 Aside의 exec 위임이 서브에이전트 대비 얹는 이점을 관측하지 못했습니다.
다만 ego를 고른다는 건 click() 래퍼를 직접 씌우고 들어간다는 뜻입니다. 이걸 안 하면 조용한 오작동을 안고 가게 됩니다. 결함의 무게로만 따지면 ego 쪽이 더 무겁고, 그런데도 ego를 기본으로 두는 건 그 결함이 우회 가능한 반면 소유권 모델은 나중에 얹을 수 없기 때문입니다.
이 기록의 한계
- 효율 우열. 순서 효과 검정을 통과하지 못했습니다. 웜런 6회가 더 필요합니다
- 표본. 능력 시험은 각 1회입니다. 반복이 없습니다
- 재현성. 대상 사이트 DOM은 A/B 테스트와 반응형 분기가 있어 같은 URL·같은 시점에도 브라우저별 초기 로드 바이트가 달랐습니다(222,927 / 252,588)
- 미션 2의 도구 귀속. Aside의 실패는 관측 실패가 라운드 상한과 겹친 결과입니다. 도구 한계로 읽으면 안 됩니다