헤드리스 브라우저
lexmount/moli · 스타 20,184 · 포크 542 · 최신 릴리스 v1.1.19.
메타데이터는 2026-10-23 GitHub 기준, 코드는 커밋 95094dd 기준입니다.
moli는 AI 에이전트가 웹페이지를 읽고 조작하도록 만든, 화면 없는 독립형 브라우저입니다. HTML을 가져오는 데서 끝나지 않고 JavaScript를 실행하고 DOM을 갱신하며, 쿠키·스토리지·네트워크 요청과 자동화 명령을 처리합니다. Chromium 프로세스를 감싼 래퍼가 아니라, V8·html5ever·Stylo 같은 부품을 조립한 별도의 브라우저 커널입니다.
moli의 핵심은 브라우저 기능을 없애는 것이 아니라, 요청에 필요하지 않은 레이아웃과 페인트 작업을 미루는 데 있습니다. 페이지 제목이나 본문을 읽는 작업과 스크린샷을 찍는 작업을 같은 비용으로 취급하지 않습니다.
DOM은 방과 복도를 적은 설계도, CSS는 자재와 배치 규칙, 레이아웃은 실제 치수표, 페인트는 완성 조감도에 가깝습니다. 에이전트가 “구매 버튼의 글자”를 읽을 때는 설계도만으로 충분할 수 있습니다.
반대로 “화면의 이 좌표를 클릭해라”에는 치수가, “지금 화면을 보여 달라”에는 그림이 필요합니다. moli는 요청 종류에 따라 이 작업들을 선택합니다. 설계도도 JavaScript에 의해 계속 바뀔 수 있다는 점이 일반 HTML 다운로드와의 차이입니다.
| 경로 | 사용자가 하는 일 | 소스에서 확인한 입구 |
|---|---|---|
| CLI 추출 | 페이지를 열고 Markdown·HTML·시맨틱 트리·PNG·PDF로 내보내기 | moli/src/cli.rs의 Commands, FetchArgs |
| 자동화 서버 | Playwright 등 외부 클라이언트가 프로토콜로 명령 전송 | moli-protocol-server/src/protocol_server/mod.rs의 ProtocolServer |
아래는 README와 CLI 정의에 따른 사용 예시입니다. moli 실행 파일이 설치된 환경에서 사용할 수 있으며, 스크린샷은 리디렉션으로 PNG 파일에 저장합니다.
# 본문을 Markdown으로 추출한다.
moli fetch --dump markdown --wait-until done https://example.com
# 모델이 읽기 쉬운 시맨틱 텍스트 트리를 얻는다.
moli fetch --dump semantic_tree_text --wait-selector body https://example.com
# 시각 출력에는 --layout을 명시한다.
moli fetch --layout --dump screenshot https://example.com > page.png
# 기존 자동화 클라이언트가 연결할 서버를 연다.
moli serve --layout
“스크린샷 한 장”이 어떤 모듈을 지나가는지, 변경 가능한 상태는 누가 소유하는지, 외부 명령은 어떤 자료형으로 바뀌는지 추적해 봅니다. 브라우저 전체를 구현할 줄 알아야 읽을 수 있는 글은 아닙니다.
상품명 수집, 문서 요약용 본문 추출, 폼 입력 확인에는 페이지의 구조와 실행 결과가 중요합니다. moli는 이런 작업에 Markdown과 시맨틱 트리를 직접 제공하고, 필요하면 같은 런타임에서 좌표와 스크린샷도 제공합니다. 기능을 두 제품으로 나누지 않고 비용이 큰 단계를 옵션으로 만든 점이 관심을 끌 만합니다.
이는 설계와 사용 사례에서 도출한 해석입니다. TrendShift Monthly #9는 2026-10-23 기준 순위이며, 스타 수는 같은 날짜 GitHub 기준입니다. 순위와 스타 수만으로 특정 기능이 인기의 원인이라고 단정할 수는 없습니다.
CLI 외에 CDP, WebDriver Classic, WebDriver BiDi를 한 서버에서 제공합니다.
소스의 build_router()에는 CDP 탐색용 /json/version, CDP WebSocket 경로,
WebDriver의 /session 경로가 함께 등록되어 있습니다. 프로토콜 지원은 클라이언트를 새로 만들 필요를 줄입니다.
| 비교 대상 | moli의 선택·장점 | 함께 봐야 할 조건 |
|---|---|---|
| 일반 HTTP 수집 | V8·DOM·이벤트·스토리지가 있어 JavaScript로 바뀌는 페이지를 다룰 수 있음 | 단순 정적 문서는 브라우저 런타임 없이도 처리 가능. 필요한 기능만 고르기 |
| Chrome Headless | DOM 중심 요청은 실제 레이아웃·페인트를 생략. 외부 ChromeDriver 없이 독립 실행 | Chrome과 같은 시각 결과·웹 기능 전체를 보장하지 않음 |
| Lightpanda·Obscura | README의 공개 웹 실험에서는 더 많은 유효 페이지를 얻었다고 보고 | 같은 실험에서 이 두 도구의 중앙값 RSS는 moli보다 작음. 우열은 작업에 따라 달라짐 |
| Playwright | README에 connectOverCDP() 연결 예시가 있음 | 서로 역할이 다름. Playwright는 자동화 클라이언트, moli는 연결 대상 엔진 |
다음은 README.md의 192개 공개 URL 크롤링 실험을 옮긴 것입니다.
프로젝트 측은 JavaScript 실행 뒤 의미 있는 본문이 나와야 성공으로 계산했다고 설명합니다.
아래 표는 프로젝트 측 측정 결과이며, 측정 환경과 성공 판정 조건을 함께 봐야 합니다.
| 브라우저 | 유효 페이지 / 성공률 | 시간 중앙값 | RSS 중앙값 |
|---|---|---|---|
| Moli | 103 / 53.6% | 1.43초 | 73 MiB |
| Chrome Headless | 101 / 52.6% | 1.43초 | 773 MiB |
| Lightpanda | 85 / 44.3% | 0.97초 | 40 MiB |
| Obscura | 57 / 29.7% | 1.30초 | 39 MiB |
성능 비교에서는 “몇 배 빠르다”보다, 같은 입력에서 무엇을 성공으로 세고 어떤 작업을 생략했는지를 먼저 봐야 합니다. 위 표에서 moli와 Chrome의 시간 중앙값은 같습니다. 가벼운 메모리 사용이 모든 페이지의 더 빠른 완료를 뜻하지는 않습니다.
README의 Lexbench 81.88% 결과는 Moli 0.1.1의 1,308개 비교 과제 결과입니다. 현재 v1.1.19의 점수로 바꾸어 읽으면 안 됩니다. 별도의 557개 과제 자원 비교도 위 192개 URL 표와 모집단이 다릅니다.
README의 WPT 약 161만 건 통과 주장 역시 프로젝트가 고른 시험 범위의 결과입니다. 전체 웹 표준 준수율이나 모든 실제 사이트의 성공률을 뜻하지 않습니다.
루트 Cargo.toml에는 91개 workspace 멤버가 있습니다.
실행 파일은 moli이고, DOM·파서·네트워크·레이아웃·저장소가 작은 Rust crate로 분리되어 있습니다.
여기서 백엔드는 일반 웹 서비스의 업무 서버가 아니라 브라우저 커널과 자동화 서버를 뜻합니다.
| 역할 | 의존성·확인 버전 | 읽은 파일·의미 |
|---|---|---|
| 언어·도구 체인 | Rust 1.96.1 / edition 2024 / resolver 3 | rust-toolchain, moli/Cargo.toml, 루트 manifest |
| CLI | clap 선언 4.5.39 → lock 4.6.0 | moli/Cargo.toml. derive로 인자·환경변수·충돌 조건 선언 |
| 비동기·서버 | Tokio lock 1.53.1 / Axum 선언 0.8.6 → lock 0.8.8 | moli, moli-renderer-v8, Cargo.lock. HTTP·WebSocket·채널 처리 |
| JavaScript | v8 152.2.0 / serde_v8 =0.320.0 | moli-renderer-v8/Cargo.toml. 일부 대상은 V8 포인터 압축 활성화 |
| HTML·XML | html5ever 선언 0.40 → lock 0.40.1 / xml5ever 선언 0.40.0 → lock 0.40.1 | moli-parser/Cargo.toml. lock에는 다른 경로의 0.39 계열도 공존 |
| 선택자·CSS | selectors 0.41 / Stylo 0.22 → lock 0.22.0 | moli-renderer-v8/Cargo.toml. Servo 계열의 선택·상속·계산 스타일 |
| 박스·텍스트 배치 | Taffy =0.15.0 / Parley =0.11.0 | moli-layout/Cargo.toml. block·flexbox·grid와 글자 배치 |
| 그리기·PDF | anyrender_vello_cpu Git 18fd67d / krilla Git 5eb4e1f | moli-paint/Cargo.toml. CPU 그림 출력과 PDF 생성 경로 |
| 전송·TLS | curl 0.4.51 / curl-sys 0.4.91 / openssl-sys 0.9 + aws-lc | moli-fetch, moli-curl manifest. libcurl 정적 빌드 기능 사용 |
| 직렬화 | Serde 1.0.228 / serde_json 선언 1.0.145 → lock 1.0.149 | 구조체와 JSON 사이 변환. 프로토콜·설정·출력에 사용 |
version = "0.8.6"은 호환 범위를 허용하는 선언이고, Cargo.lock은 해당 커밋에서 선택된 구체 버전을 기록합니다.
=0.15.0은 정확 버전을 요구합니다. 또 [patch.crates-io]가 있으면 같은 이름이어도 공식 배포본 대신 포크나 로컬 소스를 쓸 수 있습니다.실제로 V8은 vendor/v8-152.2.0으로, Stylo는 Git 리비전 2fe93b2로,
Taffy는 0a33787로 대체됩니다. Parley도 a18cfc3 포크를 씁니다.
따라서 “이 버전의 공식 문서만 보면 똑같이 재현된다”는 가정은 피해야 합니다.
moli-frontend-smoke/package.json에는 React·Vue·Angular가 함께 있습니다.
이들은 moli 관리 화면의 프레임워크가 아니라, moli가 실제 프레임워크 페이지를 제대로 실행하는지 확인하는 입력입니다.
README는 독립 페이지 1,020개의 DOM 시간선을 Chromium과 대조한다고 설명합니다. 시각적 일치 시험과는 구분합니다.
| 파일 | 실제 선언 | 역할 |
|---|---|---|
| moli-frontend-smoke/package.json | React·react-dom 19.2.8 / Vue 3.5.40 / Angular 패키지 22.0.8 | 렌더링·이벤트·갱신 등 프레임워크 동작을 자극하는 시험 페이지 |
| 같은 파일 | TypeScript 7.0.2 / esbuild 0.28.1 / Node.js >=22 | 시험 페이지의 타입 검사와 번들 생성 |
| moli-cdp-smoke/package.json | puppeteer-core 24.30.0 / Node.js >=22 | 실제 Node 자동화 클라이언트로 CDP 연결 검증 |
| moli-cdp-smoke/pyproject.toml | Python >=3.11 / playwright >=1.48,<2 | moli-cdp-smoke/uv.lock 선택값은 Playwright 1.58.0, websockets 15.0.1, pypdf 6.20.0 |
| moli-webdriver-smoke/pyproject.toml | selenium >=4.44,<5 | moli-webdriver-smoke/uv.lock 선택값은 Selenium 4.44.0. Classic·BiDi 시험 |
.github/workflows/ci.yml은 Rust 1.96.1 컨테이너에서 포맷·Clippy 검사를 하고,
nextest, CDP, WebDriver, 프론트엔드 차등 시험, 연속 탐색 시험 등을 별도 작업으로 나눕니다.
성능 비교의 base/head 실행 순서를 번갈아 정하는 코드도 있어, 항상 먼저 실행한 쪽만 유리해지는 영향을 줄이려 합니다.
.github/workflows/release.yml은 Linux·macOS·Windows의 x86_64와 aarch64를 대상으로 패키지를 만듭니다.
scripts/release.py는 빌드·심볼 제거·버전 확인·압축을 담당합니다.
저장소에는 Dockerfile이 없으며, CI에서는 Rust 컨테이너 이미지를 사용합니다.
# 루트 Cargo.toml의 release 설정 발췌
[profile.release]
panic = "abort"
strip = false
codegen-units = 1
strip = false와 릴리스 스크립트의 심볼 제거는 모순이 아닙니다.
Cargo 컴파일 단계와 배포 패키징 단계가 다릅니다. panic = "abort"는 panic 시 프로세스를 종료하는 정책이며,
모든 실패를 Rust의 Result 오류처럼 복구할 수 있다는 뜻이 아닙니다.
의존성 목록을 읽을 때는 “무엇을 쓰는가”와 함께, 제품 실행·시험·배포 중 어느 단계에 쓰는지를 구분해야 합니다.
아래 구조도는 모듈의 책임을 요약한 그림입니다. 모든 화살표가 하나의 직접 함수 호출을 뜻하지는 않습니다. 특히 서버와 렌더러 사이에는 채널·명령·응답·이벤트를 중재하는 스케줄러가 있습니다.
사용자 / AI 에이전트
|
+-- CLI: fetch ---------------------------+
| moli/src/ (CLI · 설정 · 실행 진입) |
| v
+-- Playwright / Puppeteer / Selenium moli-core
| 공개 API · 탐색 · Page
v |
moli-protocol-server |
Axum HTTP / WebSocket |
| |
CDP / WebDriver Classic / BiDi |
| |
공통 AutomationCommand + 스케줄러 ---------+
v
moli-renderer-v8
소유자 실행 영역 · V8 · 이벤트 / 작업 큐
| | |
v v v
moli-parser 네이티브 DOM moli-fetch
html5ever + Stylo CSS / moli-curl
| ^ |
+----------+ +-- 웹 서버
|
+-------------------+--------------------+
| |
구조를 읽는 요청 시각 정보를 요구하는 요청
HTML / Markdown --layout: OnDemand
시맨틱 트리 / DOM 조회 |
| v
출력 직렬화 moli-layout: Taffy + Parley
| |
FrozenLayoutTree 일시 PaintSnapshot
최신 기하 정보 moli-paint
|
PNG / PDF 등
초보자는 “프로토콜 서버가 HTTP를 받는다”와 “브라우저가 웹사이트로 HTTP를 보낸다”를 구분해야 합니다.
앞의 서버는 자동화 명령의 입구이고, 뒤의 moli-fetch·moli-curl은 페이지 리소스를 가져오는 출구입니다.
원본은 네이티브 DOM과 Stylo 스타일 상태입니다. 레이아웃은 이 원본에서 만든 별도의 계산 결과입니다.
moli-renderer-v8/src/layout_renderer/mod.rs의 build_native_layout_pass()는
DOM을 읽는 어댑터와 스타일 해석기를 조립해 레이아웃 백엔드에 전달합니다.
moli-layout/src/layout_tree/pass_result.rs의 LayoutPassResult에는
고정된 트리, 진단, 시간 측정값, 선택적인 페인트 스냅샷이 들어 있습니다.
하지만 장기간 보관 경계로 넘어가는 것은 FrozenLayoutTree입니다.
현재 문서의 글꼴 서비스 같은 부가 상태까지 모두 없어진다는 뜻은 아닙니다.
moli-renderer-v8/src/native_bridge/context_host/layout_snapshot.rs의 LatestLayoutTreeCache는
여러 버전의 트리를 쌓아 두지 않고 최신 성공 결과 하나를 담습니다. publish()의 핵심은 단순한 교체입니다.
// LatestLayoutTreeCache::publish() 본문 발췌
self.latest = Some(LatestFrozenLayout {
document,
tree: Box::new(tree),
});
여기서 Box는 트리를 힙에 보관하고 소유권을 넘기는 Rust 자료형입니다.
새 결과로 대입되면 이전 값은 소유권 규칙에 따라 정리됩니다. “캐시를 무제한 늘리지 않는다”는 정책이 자료구조에서 드러납니다.
| 요청·상황 | 실제 동작을 읽는 지점 | 학습할 경계 |
|---|---|---|
| 기본 Mock | moli-page-types/src/layout.rs, moli/src/config.rs | 실제 레이아웃 없이 호환용 기하 정보 사용. 실제 스크린샷 작업과 구분 |
| 최초 실제 기하 조회 | moli-renderer-v8/src/native_bridge/context_host/layout.rs의 ensure_initial_layout() | 트리가 없을 때 SynchronousGeometry 사유로 계산·게시 |
| 게시 이후 기하 조회 | with_latest_layout_tree_for_document() | 캐시가 남아 있으면 마지막 트리를 재사용. 글꼴 등록 변경 등으로 캐시가 비워지면 다음 기하 조회 때 다시 계산 |
| 스크린샷·명시적 갱신 | moli-renderer-v8/src/script_vm/layout.rs의 with_fresh_layout_pass_in_environment() | 새 계산 결과를 소비하고 기하 정보를 게시 |
| 인쇄 | 같은 함수의 LayoutFlushReason::Print 분기 | 임시 투영을 소비하고 최신 기하 캐시에 게시하지 않음 |
이 지점에서는 주석보다 구현을 끝까지 읽는 연습이 유익합니다.
LayoutPolicy::OnDemand의 짧은 주석은 출력 중심으로 설명하지만,
실제 ensure_initial_layout()에는 최초 기하 조회가 레이아웃을 만드는 경로가 있습니다.
README의 “최초 기하 요청” 설명은 이 구현과 맞습니다.
칠판의 내용을 사진 한 장으로 보관하는 방식입니다. 칠판을 고쳤다고 앨범 속 사진도 바뀌지는 않습니다. 기존 좌표를 읽는 요청은 사진을 볼 수 있고, 새 스크린샷은 다시 촬영합니다. 사진이 언제 찍혔는지를 아는 것이 사용 계약의 일부입니다.
스크린캐스트도 화면을 계속 그려 보관하는 구조가 아닙니다.
moli-protocol-server/src/cdp_scheduler.rs의 PageScreencastSchedule은 known_visual_state를 기억하고,
렌더러의 visual_state_token()은 문서·DOM·스타일·리소스 등의 세대 정보를 조합합니다.
README에 따르면 변화가 없는 폴링은 프레임 생성을 생략합니다. 토큰 자체는 그림 데이터가 아닙니다.
외부 클라이언트마다 명령 형식이 다르더라도 브라우저의 “탐색”, “평가”, “스크린샷” 동작까지 각각 세 번 만들 필요는 없습니다.
moli-protocol/src/automation.rs의 AutomationCommand는
Navigate, EvaluateScript, CaptureScreenshot 같은 공통 명령을 열거합니다.
// AutomationCommand의 일부만 남긴 학습용 발췌
pub enum AutomationCommand {
Navigate(DevToolsNavigateCommand),
EvaluateScript(DevToolsEvaluateScriptCommand),
CaptureScreenshot(DevToolsCaptureScreenshotCommand),
PrintToPdf(DevToolsPrintToPdfCommand),
}
각 변형은 해당 명령의 자료형을 함께 운반합니다. 문자열 하나에 매개변수를 뒤섞는 대신 컴파일러가 명령 종류와 데이터의 조합을 확인하도록 한 것입니다. 실제 enum에는 훨씬 많은 명령이 있습니다.
moli-protocol-server/src/protocol_server/mod.rs의 build_router()는 외부 경로를 등록하고,
moli-protocol-server/src/cdp_scheduler/adapter_scheduler.rs의 ProtocolAdapterScheduler는 어댑터의 실행을 중재합니다.
내부 명령과 외부 응답 표현을 나누면, 프로토콜이 늘어날 때 엔진의 핵심 동작을 공유할 수 있습니다.
moli-core/src/lib.rs는 스스로 안정적인 공개 창구라고 설명하며, 세부 구현은 분리된 crate에 둡니다.moli-renderer-v8/src/render_runtime.rs의 RenderRuntimeOwner::spawn()은
render_runtime이라는 스레드와 current-thread Tokio 런타임을 만들고, 채널에서 렌더러 명령을 받습니다.
아무 스레드나 V8과 문서를 직접 바꾸게 두지 않고, 소유자의 실행 영역으로 일을 보냅니다.
moli-local-executor/src/executor.rs의 JsLocalExecutor::run()도 같은 관점을 보여 줍니다.
이미 같은 실행 영역 안이면 바로 진행하고, 아니면 owner_lane.lock().await로 잠근 뒤 영역을 표시합니다.
이 실행기는 진입을 직렬화하는 도구이며, 그것만으로 모든 작업을 특정 OS 스레드에 옮기는 기능이라고 해석하면 안 됩니다.
외부 작업 A ---+
외부 작업 B ---+--> 명령 채널 --> 상태 소유자 --> V8 / DOM 변경
네트워크 완료 -+ |
+--> 이벤트 · 명령 결과
같은 상태의 동시 변경을 줄이고,
“누가 먼저 바꾸고 무엇을 먼저 내보냈는가”를 한 지점에서 조정한다.
서버 쪽 moli-protocol-server/src/cdp_scheduler/actor.rs는 진행 중 명령,
뒤로 미뤄진 응답, 백그라운드 이벤트를 함께 관리합니다.
queue_ready_background_inputs_before_runtime_response()는 JavaScript 실행 중 준비된 이벤트를
Runtime 응답보다 앞에 두되, 유한한 묶음을 만들어 응답이 끝없이 밀리지 않도록 합니다.
moli-owner-queue/src/owner_task_source.rs의 enqueue_local()은 뒤에,
enqueue_parser_boundary_local()은 앞에 작업을 넣습니다.
따라서 큐가 있다고 “모든 작업이 엄격한 선입선출”이라고 단정할 수 없습니다. 예외적인 우선순위도 실행 계약입니다.
웹소켓 응답이 이상한 순서로 오면 소켓 코드만 보지 마세요. 상태를 바꾸는 주인, 완료 이벤트를 모으는 큐, 응답을 풀어 주는 시점까지 추적하면 원인을 좁힐 수 있습니다.
HTML을 읽는 동안 스크립트가 DOM을 바꾸거나 추가 입력을 만들 수 있습니다.
moli-parser/src/stream.rs의 pump_parser_step()은 입력을 진행시키고,
내부 RawParserStep은 스크립트·차단 스타일시트·사용자 정의 요소·입력 소진 등을 서로 다른 상태로 표현합니다.
// stream.rs의 내부 상태 enum 발췌
enum RawParserStep {
OwnerInterrupted,
Script(NativeNodeId),
CustomElementConstruction,
BlockingStylesheet(NativeNodeId),
InputDrained,
}
ParserScriptPreparation::into_handoff()는 준비한 스크립트를
BlockingClassic, AsyncPostParse, NonAsyncPostParse,
ImportMap 등의 실행 인계 형태로 바꿉니다. 파서는 “무엇을 발견했는가”를 알려 주고 실행 계층이 이어받습니다.
moli-parser/src/html.rs의 HtmlParser는 scripting 상태를 명시적으로 받습니다.
이 설정은 파싱 규칙에 영향을 주며, 파서가 JavaScript를 직접 실행한다는 의미는 아닙니다.
“문법을 읽는 일”과 “읽은 작업을 실행하는 일”을 분리하는 사례입니다.
큰 시스템의 아키텍처는 폴더 이름보다, 상태의 주인과 계산 결과를 보관하는 경계를 따라갈 때 더 잘 보입니다.
./ 저장소 루트 ├── Cargo.toml · Cargo.lock workspace 구성 · 의존성 선택 결과 ├── rust-toolchain Rust 1.96.1 ├── moli/src/ 실행 파일 · CLI · 설정 · 출력 ├── moli-core/src/ 공개 API · Page · 탐색 · 저장 파티션 ├── moli-protocol-server/src/ HTTP / WebSocket · 스케줄러 · 종료 관리 ├── moli-protocol/src/ 공통 자동화 명령 · CDP 도메인 · 연결 상태 ├── moli-protocol-cdp/ CDP용 형식·메타데이터 등 ├── moli-protocol-webdriver-classic/ Classic 변환 계층 ├── moli-protocol-webdriver-bidi/ BiDi 변환 계층 ├── moli-renderer-v8/src/ V8 · 웹 API · 문서/작업 수명 · 렌더링 연결 ├── moli-dom/ · moli-parser/ 네이티브 문서 · 스트리밍 파싱 ├── moli-fetch/ · moli-curl/ 요청 정책 · 전송 · 연결 관리 ├── moli-layout/ 기하·텍스트 배치 · FrozenLayoutTree ├── moli-paint/ CPU 페인트 · 시각 출력 ├── moli-html2md/ DOM 기반 Markdown 변환 ├── moli-owner-queue/ 소유자 작업 큐·깨우기 ├── moli-local-executor/ JavaScript 실행 영역의 진입 제어 ├── moli-storage-service/ 저장 서비스 ├── moli-browser-profile/ 프로필 경로·잠금·지속성 경계 ├── moli-cdp-smoke/ Python·Node CDP 통합 시험 ├── moli-webdriver-smoke/ WebDriver 통합 시험 ├── moli-frontend-smoke/ 프레임워크 페이지·DOM 차등 시험 ├── moli-benchmark/ 성능·크롤링 비교 도구 ├── moli-playground/ 예제·WebMCP 등 ├── tools/wptrunner-moli/ WPT 연동 도구 ├── scripts/ 릴리스·개발 보조 코드 ├── skills/ 에이전트 사용 안내 ├── vendor/ V8 등 저장소에 포함한 외부 코드 ├── licenses/ 제삼자 고지 └── .github/workflows/ CI · 릴리스 · 회귀 비교
위 트리는 학습에 필요한 경로를 추린 것입니다. 쿠키·DNS·CSP·WebCrypto·IndexedDB·OPFS 등은 별도 crate로 더 나뉩니다.
moli/는 실행 파일 crate입니다. 본문의 moli/src/main.rs 같은 경로는 저장소 루트 기준입니다.
아래는 커밋 95094dd의 확장자별 파일 수와 물리적 줄 수입니다.
아래 표는 .git/과 vendor/를 제외하고, 빈 줄·주석·테스트·시험 HTML·생성 코드까지 포함합니다.
“개발자가 직접 쓴 구현 줄 수”나 SLOC 도구의 유효 코드 줄 수는 아닙니다.
| 언어·파일 종류 | 파일 수 | 물리적 줄 수 |
|---|---|---|
| Rust (.rs) | 4,528 | 2,291,325 |
| Python (.py) | 356 | 147,023 |
| JavaScript (.js/.mjs/.cjs/.jsx) | 970 | 73,461 |
| TypeScript (.ts/.tsx) | 56 | 16,697 |
| HTML (.html) | 1,337 | 87,424 |
| CSS (.css) | 4 | 618 |
| C/C++ (.c/.cc/.cpp/.h) | 1 | 135 |
| Shell (.sh) | 6 | 448 |
집계 조건을 바꾸면 숫자도 바뀝니다. vendor/를 포함한 Rust는 4,614개 파일·2,345,264줄입니다. 외부 코드를 포함한 수치와 제외한 수치를 섞어 읽지 마세요.
| Rust의 큰 영역 | 물리적 줄 수 | 읽기 시작할 곳 |
|---|---|---|
| moli-renderer-v8 | 1,304,503 | 전체를 읽으려 하지 말고 moli-renderer-v8/src/render_runtime.rs와 moli-renderer-v8/src/script_vm/layout.rs부터 |
| moli-protocol | 443,529 | moli-protocol/src/automation.rs에서 명령 하나 선택 |
| moli-protocol-server | 92,403 | moli-protocol-server/src/protocol_server/mod.rs의 경로 등록 |
| moli-core | 58,650 | moli-core/src/lib.rs와 moli-core/src/runtime/navigation_engine.rs |
| moli-layout | 50,752 | moli-layout/src/layout_tree/pass_result.rs의 입출력형 |
이 표에도 각 영역의 테스트 등이 포함되어 있습니다. 큰 폴더를 읽을수록 많이 배운다는 순위는 아닙니다. 처음에는 CLI의 플래그 하나를 골라 설정·내부형·동작·테스트의 네 지점을 연결하는 편이 이해하기 쉽습니다.
AutomationCommand와 파서의 상태형에서는 종류마다 필요한 값을 해당 자료형에 담습니다.
kind = "screenshot"인데 스크린샷용 인자가 없는 상태를 표현하기 어렵게 만듭니다.
처음에는 enum·match·Option·Result 순서로 읽으면 추적하기 쉽습니다.
작은 실습: 자신의 메모 앱에 Command::Create·Command::Read·
Command::Delete를 만들고, 필요한 값은 각 변형에만 담습니다. 문자열과 많은 선택적 인자로 표현한 버전과 비교해 보세요.
moli-renderer-v8/src/native_bridge/context_host/layout.rs의 ActiveLayoutPass는 중복된 레이아웃 시작을 거부하고,
Drop에서 실행 중 플래그를 되돌립니다. 중간에 ?로 반환하는 경로도 가드의 수명에 후처리를 묶을 수 있습니다.
다만 프로세스가 abort할 때까지 일반적인 Drop 실행을 보장하는 것은 아닙니다.
Drop에 쓸 수 있습니다.작은 실습: 중복 호출을 막는 작은 가드를 별도 프로젝트에서 만들고, 성공·조기 return·일반 오류에서 각각 플래그가 되돌아오는지 확인합니다.
Tokio 채널은 일을 전달하는 수단입니다. 전송 성공·수락·실행 완료·외부 자원 해제는 서로 다른 사건일 수 있습니다.
커밋 95094dd의 wait_for_resources_bounded()는 탭 종료 통지 뒤에도 I/O 후처리가 남는 문제를 다룹니다.
작은 실습: Python의 asyncio.Queue로 “일 전달”·“처리”·“후처리”를 서로 다른 로그에 남기고,
고정 시간 sleep과 상태를 기다리는 방법을 비교합니다. 구현 언어가 달라도 비동기에서 관찰할 지점은 비슷합니다.
최초 HTTP 응답은 입력입니다. 파싱 후 DOM, JavaScript 실행 후 DOM, 레이아웃, 출력 이미지는 서로 다른 관찰 대상입니다.
moli-parser와 moli-dom, V8의 연결을 읽으면 같은 URL의 “내용”에도 여러 단계가 있음을 알 수 있습니다.
작은 실습: 나중에 제목을 변경하는 자작 페이지를 사용해 HTML 원본·DOM 텍스트·화면을 각각 기록합니다. “가져왔다”는 판단에 HTTP 200만으로 충분하지 않은 이유를 자기 말로 설명해 보세요.
moli-frontend-smoke/README.md는 문서 생성·마운트·갱신 후·안정 후의 체크포인트를 설명합니다.
일부 사례는 중간 프레임의 차이도 실패로 처리합니다. 최종 DOM이 같아도 중간 이벤트 순서나 내용은 다를 수 있기 때문입니다.
작은 실습: 자작 UI의 “읽는 중 → 결과 → 재시도”를 시간순 표로 정리하고, 마지막 화면만 비교하는 시험이 놓칠 버그 하나를 찾습니다. moli의 시험을 전부 실행할 필요는 없습니다.
최신 트리 하나를 남기는 설계는 메모리 사용과 재계산을 줄입니다. 동시에 DOM 변경 직후의 좌표가 오래된 값일 수도 있습니다. 캐시를 공부한다면 “무엇을 저장하는가”와 함께 “무엇이 갱신을 일으키는가”·“어떤 읽기가 오래된 값을 허용하는가”를 적어야 합니다.
이 저장소에서 배울 핵심은 동작·비용·정보의 최신성을 함께 설계하는 방법입니다.
확인한 README에는 모든 작업에 통용되는 최소 RAM·CPU·디스크 용량 표가 없습니다. 아래는 파일에서 확인한 조건과 학습 환경에 대한 제안을 구분한 것입니다. README의 메모리 측정값을 제품의 최소 메모리 요구량으로 바꾸어 제시하지 않습니다.
| 구분 | 소스·설정에서 확인한 사실 | 준비할 때의 의미 |
|---|---|---|
| 운영체제·CPU | 릴리스 CI는 Linux GNU·macOS·Windows MSVC 각각 x86_64/aarch64를 빌드 | 실제 운영체제와 아키텍처에 맞는 산출물 확인 |
| macOS | 릴리스 CI의 MACOSX_DEPLOYMENT_TARGET=13.0 | 빌드 배포 대상 설정. 모든 구형 기기의 동작을 검증했다는 뜻은 아님 |
| Linux | .cargo/config.toml은 구형 배포판 호환을 이유로 RELR 사용을 보류 | 정확한 최저 glibc 버전은 이 설정만으로 확정할 수 없음 |
| GUI·GPU | README상 영구 창·GPU 합성기는 범위 밖. moli-paint는 Vello CPU 경로 사용 | 지원하는 소프트웨어 시각 출력은 전용 GPU 없이 처리하도록 설계 |
| Rust 소스 빌드 | 도구 체인 1.96.1. Linux CI에 CMake·libclang 설치 단계 존재 | 네이티브 의존성과 도구 체인까지 필요. 실행 파일 사용보다 준비 범위가 큼 |
| 시험 도구 | Node.js >=22, Python >=3.11. npm·uv lockfile 사용 | 이들은 해당 시험을 돌릴 때의 조건이며 일반 CLI 사용 조건과 다름 |
| 글꼴·리소스 | 시스템 글꼴 모듈과 --font·--image 등 선택 옵션 존재 | 글꼴·이미지 정책이 화면과 배치에 영향을 줌. 같은 URL만으로 출력 동일성을 보장하지 않음 |
Markdown 추출 위주라면 기본 Mock 경로부터 살펴볼 수 있습니다.
스크린샷·좌표 입력이 필요하면 --layout, 실제 이미지나 웹 글꼴까지 필요하면 해당 리소스 옵션을 추가합니다.
--resource는 선택적 리소스 계열을 한꺼번에 허용하는 옵션이므로 기능과 네트워크 비용이 같이 달라집니다.
학습용으로는 동시에 한 페이지만 다루는 실험부터 시작하는 것을 제안합니다. 페이지 수·스크립트 부하·캡처 크기를 하나씩 늘리면서 RSS, CPU 시간, 열린 파일 수를 측정하면 출처 없는 “페이지당 고정 메모리” 가정보다 자신의 작업에 맞는 예산을 구할 수 있습니다.
moli/src/main.rs는 runtime_thread_budget을 받아
worker_threads()와 max_blocking_threads()를 각각 설정합니다.
moli-protocol-server/src/runtime_thread_budget.rs의 계산은 사용 가능한 병렬 수와 4 중 작은 값입니다.
두 풀의 상한은 별도이고, 렌더러·V8·다른 작업의 스레드도 있습니다.
따라서 이 값을 보고 moli의 전체 스레드가 최대 네 개라고 해석하면 틀립니다.
렌더러의 moli-renderer-v8/src/tokio_blocking_budget.rs도 소유자 런타임의 blocking 풀에 별도 상한을 적용합니다.
moli-protocol-server/src/config.rs의 기본 바인딩은 127.0.0.1, 포트는 9222입니다.
서버 설정의 timeout은 10초이고 스크린캐스트 기본 간격은 1,000ms입니다.
CLI fetch의 --timeout 기본값 25,000ms와는 대상·단위가 다릅니다.
ProtocolServer::new_with_initial_cookies()는 메모리 저장 파티션을 엽니다.
CLI에는 --profile-dir, --http-cache-dir, --cookie-file 같은 지속성 경로가 따로 있습니다.
테스트에서 데이터를 남길지 여부도 기능 선택의 일부로 다뤄야 합니다.
.cargo/config.toml은 Linux GNU x86_64에 --no-pie를 설정하고,
주석에서 주 실행 파일의 ASLR을 포기한다고 명시합니다. 재배치 비용을 줄이려는 선택입니다.
이 설정 하나로 제품 전체의 안전성을 판정할 수는 없지만, 성능 최적화에는 다른 속성과의 교환이 있음을 보여 줍니다.
아래는 독자가 별도로 수행하는 과제입니다. 코드 수정 과제는 개인 학습용 복사본에서 진행하고, 동작 실습은 설치가 준비된 환경과 직접 만든 시험 페이지를 사용합니다.
목표: CLI 인자가 내부 설정으로 바뀌는 과정 이해.
moli/src/cli.rs에서 layout 필드와 MOLI_LAYOUT을 찾습니다.moli/src/config.rs에서 bool 값이 LayoutPolicy로 바뀌는 분기를 찾습니다.moli-page-types/src/layout.rs의 기본값과 uses_real_layout()을 읽습니다.완료 기준: “인자 → 설정 → 내부 enum → 허용되는 작업” 네 칸의 표를 작성합니다. Mock 모드에서 나온 좌표가 실제 CSS 배치의 증거인지도 설명합니다. 이 과제는 읽기만으로 끝낼 수 있습니다.
목표: 사람이 읽는 Markdown과 구조 중심 시맨틱 트리의 차이 이해.
제목·링크·버튼·입력 필드가 있는 간단한 자기 페이지를 준비합니다. 아래 명령의 URL을 자신의 시험 페이지로 바꾸고 두 출력을 비교하세요. 예시 자체는 표준 예제 주소를 사용합니다.
moli fetch --dump markdown --wait-selector body https://example.com
moli fetch --dump semantic_tree_text --wait-selector body https://example.com
완료 기준: 각 출력에 남는 정보와 생략되는 정보를 세 가지씩 적습니다. 요약용 문서와 버튼 선택용 입력 중 어느 작업에 어떤 표현이 더 적합한지도 이유를 씁니다.
목표: 최신 DOM과 마지막 레이아웃이 다를 수 있음을 관찰.
moli serve --layout에 CDP 클라이언트를 연결합니다.getBoundingClientRect()의 너비를 읽습니다.Page.captureScreenshot을 수행하고 다시 너비를 읽어 시점을 비교합니다.완료 기준: 요청 순서·DOM 값·기하 값·예상 갱신 원인을 표로 정리합니다. 관련 시험이나 코드에서 추가 갱신 조건이 있는지도 확인합니다. 결과를 보기 전에 “모든 읽기가 재계산할 것”이라고 가정하지 마세요.
읽을 곳: ensure_initial_layout(), LatestLayoutTreeCache::publish(),
with_fresh_layout_pass_in_environment(). 예상과 다르면 값보다 먼저 호출 순서를 검토합니다.
목표: 일시적인 후처리와 지속적인 누수를 구분.
moli-cdp-smoke/moli_cdp_smoke/groups/target_lifecycle.py의
assert_resources_bounded()와 wait_for_resources_bounded()를 읽습니다.
Linux의 실제 프로세스를 띄우지 않고도, 아래 표본을 종이에 대입해 판단할 수 있습니다.
# 학습용 표본. 값은 테스트의 baseline 예에서 가져왔다.
baseline = {"fds": 24, "eventpoll": 7, "eventfd": 4, "threads": 20}
pending = {"fds": 40, "eventpoll": 7, "eventfd": 4, "threads": 20}
settled = {"fds": 24, "eventpoll": 7, "eventfd": 4, "threads": 20}
완료 기준: 첫 표본이 초과해도 다음 표본에서 통과할 수 있는 이유, 그리고 계속 초과하는 표본은 왜 실패해야 하는지를 설명합니다. 허용량은 FD +8, eventpoll +2, eventfd +2, threads +2입니다. 프로필 경로가 있으면 FD 비교에서 프로필 파일의 FD를 제외합니다.
검사는 반복 횟수에 비례해 예산을 늘리지 않습니다.
moli-cdp-smoke/moli_cdp_smoke/assertions.py의 wait_until() 기본값은 총 10초·간격 50ms입니다.
기다리는 시간을 허용하는 것과 누수 허용치를 늘리는 것은 다릅니다.
목표: 여러 입구에서 같은 실행 로직을 공유하는 구조 설계.
별도 학습 프로젝트에서 ReadTitle·ReadText 두 명령만 설계합니다.
CLI 문자열 입력과 JSON 입력을 같은 내부 enum으로 변환하고, 실행 함수와 출력 어댑터를 분리합니다.
시작은 실제 브라우저 대신 메모리 문서 한 개여도 충분합니다.
완료 기준: 내부 실행 로직을 바꾸지 않고 두 입구가 같은 결과를 내야 합니다. 잘못된 명령의 오류도 각각의 외부 형식으로 변환되어야 합니다. 이어서 큐를 넣어 순서가 바뀌는 경우를 관찰해 보세요.
좋은 실습 결과물은 실행 성공 화면보다, 관찰한 결과를 소스의 조건문과 자료형으로 설명하는 기록입니다.
Rust와 브라우저 내부를 모두 처음 접한다면 전부 한 번에 읽지 않아도 됩니다. 아래는 주 3~5시간 정도를 가정한 학습 제안입니다. 이해가 덜 된 주차는 반복하고, 각 주차마다 남길 결과물을 하나로 정하면 다음 단계로 넘어갈 기준이 생깁니다.
| 주차 | 학습 주제·읽을 소스 | 완료 결과물 |
|---|---|---|
| 1주 | Rust 기본형·소유권·enum·Result. moli/src/cli.rs, moli-page-types/src/layout.rs | 플래그에서 정책까지의 흐름도. Mock과 OnDemand를 예제로 설명 |
| 2주 | HTTP 응답·HTML 파싱·DOM. moli-parser/src/html.rs, moli-parser/src/stream.rs, moli-dom/src/lib.rs | 입력 HTML·실행 후 DOM·화면을 구분한 세 칸 기록 |
| 3주 | async/await·채널·소유자 실행. moli-renderer-v8/src/render_runtime.rs, moli-local-executor/src/executor.rs | 요청 접수·실행·응답·정리의 시퀀스 그림 |
| 4주 | CSS 배치·텍스트·스냅샷. moli-renderer-v8/src/layout_renderer/mod.rs, moli-renderer-v8/src/native_bridge/context_host/layout_snapshot.rs | 원본과 계산 결과의 수명 표. 기하 정보 갱신 조건 정리 |
| 5주 | CDP·WebDriver·어댑터. moli-protocol/src/automation.rs, moli-protocol-server/src/protocol_server/mod.rs | 한 명령의 외부 입력·내부형·응답 형식 매핑 표 |
| 6주 | 차등 시험·자원 예산·재현성. moli-cdp-smoke/moli_cdp_smoke/groups/target_lifecycle.py, .github/workflows/ci.yml, 프론트엔드 시험 README | 성능 측정 계획과 실패 기준. 두 입구를 가진 작은 자동화 프로젝트 |
moli-fetch → 파서 → moli-html2md. 출력 품질과 대기 조건을 중심으로 읽습니다.큰 공장의 모든 방을 먼저 외우기보다, 주문서 한 장이 들어와 제품 하나가 나갈 때까지 따라가 보세요.
moli에서는 --dump screenshot이나 --dump markdown 하나가 그 주문서입니다.
이 세 문장이 명확하면 함수 이름을 많이 외우지 않아도 다음 파일을 고를 수 있습니다. 모르는 라이브러리는 공식 문서에서 그 자료형의 역할만 보충한 뒤 다시 호출 지점으로 돌아오는 방식이 좋습니다.
| 용어 | 쉬운 설명 |
|---|---|
| 헤드리스 브라우저 | 사람이 보는 창 없이 웹 런타임을 실행하는 브라우저. 창이 없다고 JavaScript·배치·그림 기능이 모두 없는 것은 아닙니다. |
| 브라우저 커널 | 파싱·실행·네트워크·문서·렌더링을 조정하는 중심 시스템. moli는 Chromium 외부 프로세스를 감싼 방식과 구분됩니다. |
| DOM | 요소·텍스트·부모 자식 관계를 가진 문서 트리. JavaScript로 바뀔 수 있는 살아 있는 자료입니다. |
| 계산 스타일 | CSS 우선순위와 상속 등을 적용해 요소에 정한 스타일. 실제 배치 치수와 같은 개념은 아닙니다. |
| 레이아웃 | 요소와 글자를 어느 위치에 얼마나 크게 놓을지 계산하는 단계. |
| 페인트 | 배치된 모양·글자·이미지를 시각 출력으로 그리는 단계. |
| Mock 기하 정보 | 실제 CSS 배치 계산 대신 호환을 위해 일정한 규칙으로 정한 값. 실제 화면 좌표의 증거로 쓰면 안 됩니다. |
| FrozenLayoutTree | 한 번 계산한 기하 정보를 고정해 보관하는 트리. 변경 가능한 DOM 원본과 수명이 다릅니다. |
| V8 · Isolate | V8은 JavaScript 실행 엔진, Isolate는 그 실행 상태를 분리하는 단위입니다. moli의 상태 소유·스케줄링과 함께 읽습니다. |
| CDP | Chrome DevTools 계열 자동화 명령·이벤트 규약. moli는 이 규약을 구현해 클라이언트의 연결을 받습니다. |
| WebDriver Classic | 세션과 HTTP 명령을 중심으로 브라우저를 조작하는 규약. Selenium 같은 도구와 연결됩니다. |
| WebDriver BiDi | 클라이언트와 브라우저가 양방향 메시지를 주고받는 WebDriver 규약. 명령뿐 아니라 이벤트 전달도 중요합니다. |
| 어댑터 | 외부 프로토콜 표현을 내부 명령으로 번역하고 결과를 다시 외부 형식으로 바꾸는 부품. |
| 파사드 | 많은 내부 모듈의 기능을 비교적 단순한 공개 입구로 제공하는 설계. |
| 상태 기계 | 가능한 상태와 그 사이의 이동을 정해 둔 모델. 파서의 스크립트 발견·중단·재개를 표현하는 데 적합합니다. |
| 소유자 실행 영역 | 특정 상태를 바꾸는 작업을 제어하는 실행 경계. 같은 상태를 아무 작업에서나 동시에 수정하지 않도록 합니다. |
| 채널 | 작업 사이에 명령·결과·깨우기 신호를 보내는 통로. 메시지를 보냈다는 사실과 처리가 끝났다는 사실은 다릅니다. |
| microtask | Promise 반응 등 JavaScript의 작은 후속 작업을 정해진 시점에 실행하는 체계. DOM 갱신 관찰 순서에 영향을 줍니다. |
| RAII · Drop | 자원의 수명을 값의 수명에 묶는 방식과 Rust의 정리 동작. 보통의 종료·오류 경로에서 후처리를 돕습니다. |
| 차등 시험 | 같은 입력을 두 구현에 넣어 결과를 비교하는 시험. 정규화 규칙과 관찰 시점이 결과의 의미를 정합니다. |
| WPT | 웹 플랫폼 동작을 시험하는 공개 시험 모음. 선택한 범위와 통과 건수를 함께 봐야 합니다. |
| RSS · PSS | 프로세스 메모리를 보는 서로 다른 지표. 공유 메모리 계산 방식이 달라 숫자를 바로 섞어 비교하면 안 됩니다. |
| FD · eventpoll · eventfd | 운영체제의 파일·소켓·이벤트 자원과 관련된 관측값. Linux 자원 회수 시험에서 증가량을 감시합니다. |
| Cargo workspace | 여러 Rust 패키지의 의존성·빌드·정책을 한 저장소에서 관리하는 단위. 이 레포는 91개 멤버로 구성됩니다. |
| lockfile · patch | lockfile은 선택한 의존성 버전을 기록합니다. patch는 같은 의존성 이름이 가리키는 소스를 포크나 로컬 코드로 바꿀 수 있습니다. |
| shallow clone | 커밋 이력을 일부만 가져온 복제본. 현재 파일 분석은 가능하지만 전체 역사·기여량 통계에는 부족합니다. |
| WebMCP | 사이트가 제공하는 도구를 에이전트가 발견하고 호출하는 방향의 인터페이스. 이 레포에는 관련 런타임과 playground가 있습니다. |
단어를 외우기보다 이 글의 예와 짝지어 보세요. 스냅샷은 LatestLayoutTreeCache,
어댑터는 AutomationCommand로의 변환, 상태 기계는 RawParserStep처럼 기억하면 됩니다.
아래 링크는 모두 커밋 95094dd 기준입니다.
같은 버전의 코드와 설정을 함께 살펴볼 수 있습니다.
| 항목 | 값 | 확인 근거·범위 |
|---|---|---|
| 선정 | TrendShift Monthly #9 | 2026-10-23 TrendShift 기준 |
| 스타 / 포크 | 20,184 / 542 | 2026-10-23 GitHub 기준 |
| 생성일 | 2026-08-10 | 2026-10-23 GitHub 기준 |
| 주 언어 | Rust | 2026-10-23 GitHub 기준 |
| 최신 릴리스 | v1.1.19 · 2026-10-21 | 2026-10-23 GitHub 기준. moli/Cargo.toml의 버전도 1.1.19 |
| 최근 push | 2026-10-23 | 2026-10-23 GitHub 기준 |
| 분석 HEAD | 95094dd · 2026-10-23 | 해당 커밋의 Git 로그 |
| 최근 커밋 내용 | 고정된 자원 예산 안에서 타깃 IO 회수를 기다리도록 smoke 검사 수정 | 제목: fix(smoke): wait for target IO reclamation within fixed resource budgets |
| 코드 규모 | Rust 약 229만 줄 / Python 약 14.7만 줄 / JS 약 7.3만 줄 / TS 약 1.7만 줄 | vendor 제외 물리적 줄 수. 빈 줄·주석·테스트·생성 코드 포함. 상세는 5절 |
| workspace | 91개 멤버 | 루트 Cargo.toml의 명시적 목록 |
| 라이선스 | Apache-2.0 | 2026-10-23 GitHub 기준. 소스 기본 선언은 README.md, workspace.package, license-metadata.json의 Apache-2.0 OR MIT이며, 제삼자 예외 존재 |
| 성능 수치 | README의 프로젝트 측 측정 결과 | 브라우저 버전·입력 과제·측정 지표를 구분해 비교 |
소스의 동작과 측정 수치는 적용 범위가 다릅니다. 코드 설명은 커밋 95094dd 기준입니다.
GitHub 메타데이터는 2026-10-23 기준이며, README의 성능·사이트 성공률은 각 실험의 버전과 조건에 한정됩니다.
배포 패키지의 동작과 실제 사이트 호환성은 사용할 환경에서 확인해야 합니다.
Chrome 대체 범위는 작업별로 확인해야 합니다. README는 픽셀 단위 Chrome 일치, 고충실도 Canvas·WebGL·미디어 재생을 목표로 하지 않는다고 밝힙니다. 지원하지 않는 프로토콜 경로가 있다는 설명도 있으므로, 연결된 클라이언트의 모든 기능을 보장한다고 읽지 마세요.
기하 정보의 시점에 주의하세요. 기본 Mock 값은 실제 배치가 아니며, OnDemand에서도 마지막으로 게시한 스냅샷을 재사용할 수 있습니다. DOM 수정과 좌표 조회 사이의 갱신 계약을 확인해야 합니다.
라이선스 표기는 실제 파일과 함께 확인하세요. API가 선택한 단일 표시와 소스의 이중 라이선스 선언이 다릅니다.
기본 적용 문구 외에 licenses/, 파일별 고지, 외부 코드·시험 자료의 예외도 존재합니다.