본문으로 건너뛰기
5분 AI.
TRENDSHIFT MONTHLY #9 딥다이브 · 2026-10-23 분석

moli 딥다이브
— 웹의 구조를 읽고, 화면은 필요할 때만 그린다

헤드리스 브라우저

빠른 판단

무엇
JavaScript·DOM·스토리지를 처리하며 필요에 따라 레이아웃·화면 출력을 수행하는 독립 Rust 브라우저
이럴 때
에이전트가 웹페이지를 읽고 조작할 때 구조 추출만 할지 시각적 렌더링도 할지 작업별로 선택할 때
AI 에이전트가 웹을 읽고 조작하려면 꼭 화면을 계속 그려야 할까요? moli는 Rust로 만든 독립형 헤드리스 브라우저로, JavaScript를 실행하고 DOM·CSS를 처리하면서 시각 작업의 비용은 필요할 때 지불합니다. 이 글은 CLI에서 파서·V8·프로토콜 스케줄러·레이아웃까지 소스를 따라가며, 큰 시스템을 비용과 소유권으로 나누는 법을 공부합니다. 저장소 lexmount/moli · 스타 20,184 · 포크 542 · 최신 릴리스 v1.1.19. 메타데이터는 2026-10-23 GitHub 기준, 코드는 커밋 95094dd 기준입니다.
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

브라우저의 모든 일을 매번 수행하지 않는 브라우저

moli는 AI 에이전트가 웹페이지를 읽고 조작하도록 만든, 화면 없는 독립형 브라우저입니다. HTML을 가져오는 데서 끝나지 않고 JavaScript를 실행하고 DOM을 갱신하며, 쿠키·스토리지·네트워크 요청과 자동화 명령을 처리합니다. Chromium 프로세스를 감싼 래퍼가 아니라, V8·html5ever·Stylo 같은 부품을 조립한 별도의 브라우저 커널입니다.

moli의 핵심은 브라우저 기능을 없애는 것이 아니라, 요청에 필요하지 않은 레이아웃과 페인트 작업을 미루는 데 있습니다. 페이지 제목이나 본문을 읽는 작업과 스크린샷을 찍는 작업을 같은 비용으로 취급하지 않습니다.

용어
헤드리스 브라우저 · DOM · 레이아웃 · 페인트
헤드리스는 사람이 보는 창 없이 브라우저를 실행한다는 뜻입니다. DOM은 페이지 요소의 관계를 저장한 나무, 레이아웃은 각 요소의 위치와 크기를 계산하는 과정, 페인트는 그 결과를 픽셀로 그리는 과정입니다. 창이 없다는 사실만으로 뒤의 두 과정이 항상 사라지는 것은 아닙니다.
한 문장 비유

설계도에서 방 이름을 찾을 때마다, 건물 조감도를 다시 그릴 필요는 없습니다

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
이 글의 학습 목표
작은 기능에서 큰 시스템의 설계 원칙을 읽기

“스크린샷 한 장”이 어떤 모듈을 지나가는지, 변경 가능한 상태는 누가 소유하는지, 외부 명령은 어떤 자료형으로 바뀌는지 추적해 봅니다. 브라우저 전체를 구현할 줄 알아야 읽을 수 있는 글은 아닙니다.

2왜 주목받는가

에이전트의 읽기 작업에 맞춘 비용 구조, 그리고 검증해야 할 호환성

2-1. 자동화의 결과물이 꼭 이미지일 필요는 없다

상품명 수집, 문서 요약용 본문 추출, 폼 입력 확인에는 페이지의 구조와 실행 결과가 중요합니다. moli는 이런 작업에 Markdown과 시맨틱 트리를 직접 제공하고, 필요하면 같은 런타임에서 좌표와 스크린샷도 제공합니다. 기능을 두 제품으로 나누지 않고 비용이 큰 단계를 옵션으로 만든 점이 관심을 끌 만합니다.

이는 설계와 사용 사례에서 도출한 해석입니다. TrendShift Monthly #9는 2026-10-23 기준 순위이며, 스타 수는 같은 날짜 GitHub 기준입니다. 순위와 스타 수만으로 특정 기능이 인기의 원인이라고 단정할 수는 없습니다.

2-2. 기존 자동화 도구와 연결하는 세 가지 언어

CLI 외에 CDP, WebDriver Classic, WebDriver BiDi를 한 서버에서 제공합니다. 소스의 build_router()에는 CDP 탐색용 /json/version, CDP WebSocket 경로, WebDriver의 /session 경로가 함께 등록되어 있습니다. 프로토콜 지원은 클라이언트를 새로 만들 필요를 줄입니다.

용어
프로토콜 호환성
두 프로그램이 명령·응답·이벤트의 형식과 의미를 함께 지키는 성질입니다. “버튼을 눌러라”라는 요청을 받는 것뿐 아니라, 어느 탭에서 실행하고 언제 응답하며 어떤 이벤트를 먼저 보내는지도 포함합니다. CDP 연결 성공만으로 Chrome의 모든 동작이 같다고 볼 수는 없습니다.

2-3. 경쟁 도구와 비교할 때 보는 세 가지 축

비교 대상moli의 선택·장점함께 봐야 할 조건
일반 HTTP 수집V8·DOM·이벤트·스토리지가 있어 JavaScript로 바뀌는 페이지를 다룰 수 있음단순 정적 문서는 브라우저 런타임 없이도 처리 가능. 필요한 기능만 고르기
Chrome HeadlessDOM 중심 요청은 실제 레이아웃·페인트를 생략. 외부 ChromeDriver 없이 독립 실행Chrome과 같은 시각 결과·웹 기능 전체를 보장하지 않음
Lightpanda·ObscuraREADME의 공개 웹 실험에서는 더 많은 유효 페이지를 얻었다고 보고같은 실험에서 이 두 도구의 중앙값 RSS는 moli보다 작음. 우열은 작업에 따라 달라짐
PlaywrightREADME에 connectOverCDP() 연결 예시가 있음서로 역할이 다름. Playwright는 자동화 클라이언트, moli는 연결 대상 엔진

2-4. README 수치는 어떤 실험의 결과인가

다음은 README.md의 192개 공개 URL 크롤링 실험을 옮긴 것입니다. 프로젝트 측은 JavaScript 실행 뒤 의미 있는 본문이 나와야 성공으로 계산했다고 설명합니다. 아래 표는 프로젝트 측 측정 결과이며, 측정 환경과 성공 판정 조건을 함께 봐야 합니다.

브라우저유효 페이지 / 성공률시간 중앙값RSS 중앙값
Moli103 / 53.6%1.43초73 MiB
Chrome Headless101 / 52.6%1.43초773 MiB
Lightpanda85 / 44.3%0.97초40 MiB
Obscura57 / 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만 건 통과 주장 역시 프로젝트가 고른 시험 범위의 결과입니다. 전체 웹 표준 준수율이나 모든 실제 사이트의 성공률을 뜻하지 않습니다.

3기술 스택 전체 지도

브라우저 엔진·시험 페이지·빌드 도구를 나누어 의존성을 읽기

루트 Cargo.toml에는 91개 workspace 멤버가 있습니다. 실행 파일은 moli이고, DOM·파서·네트워크·레이아웃·저장소가 작은 Rust crate로 분리되어 있습니다. 여기서 백엔드는 일반 웹 서비스의 업무 서버가 아니라 브라우저 커널과 자동화 서버를 뜻합니다.

3-1. 백엔드: Rust 위에 얹힌 브라우저 부품

역할의존성·확인 버전읽은 파일·의미
언어·도구 체인Rust 1.96.1 / edition 2024 / resolver 3rust-toolchain, moli/Cargo.toml, 루트 manifest
CLIclap 선언 4.5.39 → lock 4.6.0moli/Cargo.toml. derive로 인자·환경변수·충돌 조건 선언
비동기·서버Tokio lock 1.53.1 / Axum 선언 0.8.6 → lock 0.8.8moli, moli-renderer-v8, Cargo.lock. HTTP·WebSocket·채널 처리
JavaScriptv8 152.2.0 / serde_v8 =0.320.0moli-renderer-v8/Cargo.toml. 일부 대상은 V8 포인터 압축 활성화
HTML·XMLhtml5ever 선언 0.40 → lock 0.40.1 / xml5ever 선언 0.40.0 → lock 0.40.1moli-parser/Cargo.toml. lock에는 다른 경로의 0.39 계열도 공존
선택자·CSSselectors 0.41 / Stylo 0.22 → lock 0.22.0moli-renderer-v8/Cargo.toml. Servo 계열의 선택·상속·계산 스타일
박스·텍스트 배치Taffy =0.15.0 / Parley =0.11.0moli-layout/Cargo.toml. block·flexbox·grid와 글자 배치
그리기·PDFanyrender_vello_cpu Git 18fd67d / krilla Git 5eb4e1fmoli-paint/Cargo.toml. CPU 그림 출력과 PDF 생성 경로
전송·TLScurl 0.4.51 / curl-sys 0.4.91 / openssl-sys 0.9 + aws-lcmoli-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 포크를 씁니다. 따라서 “이 버전의 공식 문서만 보면 똑같이 재현된다”는 가정은 피해야 합니다.

3-2. 프론트엔드: 제품 화면 대신 호환성 시험 페이지

moli-frontend-smoke/package.json에는 React·Vue·Angular가 함께 있습니다. 이들은 moli 관리 화면의 프레임워크가 아니라, moli가 실제 프레임워크 페이지를 제대로 실행하는지 확인하는 입력입니다. README는 독립 페이지 1,020개의 DOM 시간선을 Chromium과 대조한다고 설명합니다. 시각적 일치 시험과는 구분합니다.

파일실제 선언역할
moli-frontend-smoke/package.jsonReact·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.jsonpuppeteer-core 24.30.0 / Node.js >=22실제 Node 자동화 클라이언트로 CDP 연결 검증
moli-cdp-smoke/pyproject.tomlPython >=3.11 / playwright >=1.48,<2moli-cdp-smoke/uv.lock 선택값은 Playwright 1.58.0, websockets 15.0.1, pypdf 6.20.0
moli-webdriver-smoke/pyproject.tomlselenium >=4.44,<5moli-webdriver-smoke/uv.lock 선택값은 Selenium 4.44.0. Classic·BiDi 시험

3-3. 인프라·빌드: 재현 가능한 버전과 회귀 비교

.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 오류처럼 복구할 수 있다는 뜻이 아닙니다.

의존성 목록을 읽을 때는 “무엇을 쓰는가”와 함께, 제품 실행·시험·배포 중 어느 단계에 쓰는지를 구분해야 합니다.

4아키텍처 심화 분석

명령의 이동 경로, 계산 결과의 수명, 상태를 바꾸는 주인을 찾기

아래 구조도는 모듈의 책임을 요약한 그림입니다. 모든 화살표가 하나의 직접 함수 호출을 뜻하지는 않습니다. 특히 서버와 렌더러 사이에는 채널·명령·응답·이벤트를 중재하는 스케줄러가 있습니다.

사용자 / 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은 페이지 리소스를 가져오는 출구입니다.

4-1. 패턴 ① 원본 상태 하나와 필요할 때 만드는 투영

원본은 네이티브 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 자료형입니다. 새 결과로 대입되면 이전 값은 소유권 규칙에 따라 정리됩니다. “캐시를 무제한 늘리지 않는다”는 정책이 자료구조에서 드러납니다.

요청·상황실제 동작을 읽는 지점학습할 경계
기본 Mockmoli-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에 따르면 변화가 없는 폴링은 프레임 생성을 생략합니다. 토큰 자체는 그림 데이터가 아닙니다.

4-2. 패턴 ② 프로토콜 어댑터와 타입이 있는 공통 명령

외부 클라이언트마다 명령 형식이 다르더라도 브라우저의 “탐색”, “평가”, “스크린샷” 동작까지 각각 세 번 만들 필요는 없습니다. 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에 둡니다.

4-3. 패턴 ③ 상태 소유자에게 메시지를 보내고 실행 순서를 통제

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()은 앞에 작업을 넣습니다. 따라서 큐가 있다고 “모든 작업이 엄격한 선입선출”이라고 단정할 수 없습니다. 예외적인 우선순위도 실행 계약입니다.

다른 프로젝트에도 적용할 수 있는 질문

웹소켓 응답이 이상한 순서로 오면 소켓 코드만 보지 마세요. 상태를 바꾸는 주인, 완료 이벤트를 모으는 큐, 응답을 풀어 주는 시점까지 추적하면 원인을 좁힐 수 있습니다.

4-4. 패턴 ④ 파싱을 한 번에 끝내지 않는 상태 기계

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를 직접 실행한다는 의미는 아닙니다. “문법을 읽는 일”과 “읽은 작업을 실행하는 일”을 분리하는 사례입니다.

큰 시스템의 아키텍처는 폴더 이름보다, 상태의 주인과 계산 결과를 보관하는 경계를 따라갈 때 더 잘 보입니다.

5디렉토리 구조 해부

91개 crate를 전부 읽기 전에 만드는 읽기 지도
./                            저장소 루트
├── 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 같은 경로는 저장소 루트 기준입니다.

5-1. 코드 규모와 집계 범위

아래는 커밋 95094dd의 확장자별 파일 수와 물리적 줄 수입니다. 아래 표는 .git/과 vendor/를 제외하고, 빈 줄·주석·테스트·시험 HTML·생성 코드까지 포함합니다. “개발자가 직접 쓴 구현 줄 수”나 SLOC 도구의 유효 코드 줄 수는 아닙니다.

언어·파일 종류파일 수물리적 줄 수
Rust (.rs)4,5282,291,325
Python (.py)356147,023
JavaScript (.js/.mjs/.cjs/.jsx)97073,461
TypeScript (.ts/.tsx)5616,697
HTML (.html)1,33787,424
CSS (.css)4618
C/C++ (.c/.cc/.cpp/.h)1135
Shell (.sh)6448

집계 조건을 바꾸면 숫자도 바뀝니다. vendor/를 포함한 Rust는 4,614개 파일·2,345,264줄입니다. 외부 코드를 포함한 수치와 제외한 수치를 섞어 읽지 마세요.

5-2. 어디에 읽는 시간을 쓸까

Rust의 큰 영역물리적 줄 수읽기 시작할 곳
moli-renderer-v81,304,503전체를 읽으려 하지 말고 moli-renderer-v8/src/render_runtime.rs와 moli-renderer-v8/src/script_vm/layout.rs부터
moli-protocol443,529moli-protocol/src/automation.rs에서 명령 하나 선택
moli-protocol-server92,403moli-protocol-server/src/protocol_server/mod.rs의 경로 등록
moli-core58,650moli-core/src/lib.rs와 moli-core/src/runtime/navigation_engine.rs
moli-layout50,752moli-layout/src/layout_tree/pass_result.rs의 입출력형

이 표에도 각 영역의 테스트 등이 포함되어 있습니다. 큰 폴더를 읽을수록 많이 배운다는 순위는 아닙니다. 처음에는 CLI의 플래그 하나를 골라 설정·내부형·동작·테스트의 네 지점을 연결하는 편이 이해하기 쉽습니다.

6학습 포인트 (기술별)

읽어서 얻은 지식을 작은 자작 프로그램으로 옮기기

6-1. Rust: enum으로 잘못된 조합 줄이기

AutomationCommand와 파서의 상태형에서는 종류마다 필요한 값을 해당 자료형에 담습니다. kind = "screenshot"인데 스크린샷용 인자가 없는 상태를 표현하기 어렵게 만듭니다. 처음에는 enum·match·Option·Result 순서로 읽으면 추적하기 쉽습니다.

작은 실습: 자신의 메모 앱에 Command::Create·Command::Read· Command::Delete를 만들고, 필요한 값은 각 변형에만 담습니다. 문자열과 많은 선택적 인자로 표현한 버전과 비교해 보세요.

6-2. 소유권과 RAII: 후처리를 경로마다 쓰지 않기

moli-renderer-v8/src/native_bridge/context_host/layout.rs의 ActiveLayoutPass는 중복된 레이아웃 시작을 거부하고, Drop에서 실행 중 플래그를 되돌립니다. 중간에 ?로 반환하는 경로도 가드의 수명에 후처리를 묶을 수 있습니다. 다만 프로세스가 abort할 때까지 일반적인 Drop 실행을 보장하는 것은 아닙니다.

용어
RAII
값의 생존 기간에 자원의 획득·해제를 묶는 생각입니다. 파일·잠금·실행 중 플래그 등을 처리의 마지막에 정리하는 일을 잊지 않게 도와줍니다. Rust에서는 값이 소멸할 때의 동작을 Drop에 쓸 수 있습니다.

작은 실습: 중복 호출을 막는 작은 가드를 별도 프로젝트에서 만들고, 성공·조기 return·일반 오류에서 각각 플래그가 되돌아오는지 확인합니다.

6-3. 비동기: await가 있다고 모든 후처리가 끝난 것은 아니다

Tokio 채널은 일을 전달하는 수단입니다. 전송 성공·수락·실행 완료·외부 자원 해제는 서로 다른 사건일 수 있습니다. 커밋 95094dd의 wait_for_resources_bounded()는 탭 종료 통지 뒤에도 I/O 후처리가 남는 문제를 다룹니다.

작은 실습: Python의 asyncio.Queue로 “일 전달”·“처리”·“후처리”를 서로 다른 로그에 남기고, 고정 시간 sleep과 상태를 기다리는 방법을 비교합니다. 구현 언어가 달라도 비동기에서 관찰할 지점은 비슷합니다.

6-4. 브라우저: HTML 문자열과 살아 있는 문서 구분하기

최초 HTTP 응답은 입력입니다. 파싱 후 DOM, JavaScript 실행 후 DOM, 레이아웃, 출력 이미지는 서로 다른 관찰 대상입니다. moli-parser와 moli-dom, V8의 연결을 읽으면 같은 URL의 “내용”에도 여러 단계가 있음을 알 수 있습니다.

작은 실습: 나중에 제목을 변경하는 자작 페이지를 사용해 HTML 원본·DOM 텍스트·화면을 각각 기록합니다. “가져왔다”는 판단에 HTTP 200만으로 충분하지 않은 이유를 자기 말로 설명해 보세요.

6-5. 테스트: 최종 결과와 함께 중간 계약 살피기

moli-frontend-smoke/README.md는 문서 생성·마운트·갱신 후·안정 후의 체크포인트를 설명합니다. 일부 사례는 중간 프레임의 차이도 실패로 처리합니다. 최종 DOM이 같아도 중간 이벤트 순서나 내용은 다를 수 있기 때문입니다.

작은 실습: 자작 UI의 “읽는 중 → 결과 → 재시도”를 시간순 표로 정리하고, 마지막 화면만 비교하는 시험이 놓칠 버그 하나를 찾습니다. moli의 시험을 전부 실행할 필요는 없습니다.

6-6. 성능 설계: 캐시의 유무와 갱신 계약 함께 설명하기

최신 트리 하나를 남기는 설계는 메모리 사용과 재계산을 줄입니다. 동시에 DOM 변경 직후의 좌표가 오래된 값일 수도 있습니다. 캐시를 공부한다면 “무엇을 저장하는가”와 함께 “무엇이 갱신을 일으키는가”·“어떤 읽기가 오래된 값을 허용하는가”를 적어야 합니다.

이 저장소에서 배울 핵심은 동작·비용·정보의 최신성을 함께 설계하는 방법입니다.

7하드웨어 / 시스템 요구사항

실행 파일 사용과 엔진 전체 빌드를 구분해 준비하기

확인한 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·GPUREADME상 영구 창·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만으로 출력 동일성을 보장하지 않음

7-1. 읽기 중심과 시각 중심은 자원 계획이 다르다

Markdown 추출 위주라면 기본 Mock 경로부터 살펴볼 수 있습니다. 스크린샷·좌표 입력이 필요하면 --layout, 실제 이미지나 웹 글꼴까지 필요하면 해당 리소스 옵션을 추가합니다. --resource는 선택적 리소스 계열을 한꺼번에 허용하는 옵션이므로 기능과 네트워크 비용이 같이 달라집니다.

학습용으로는 동시에 한 페이지만 다루는 실험부터 시작하는 것을 제안합니다. 페이지 수·스크립트 부하·캡처 크기를 하나씩 늘리면서 RSS, CPU 시간, 열린 파일 수를 측정하면 출처 없는 “페이지당 고정 메모리” 가정보다 자신의 작업에 맞는 예산을 구할 수 있습니다.

용어
RSS · PSS · 파일 디스크립터
RSS는 프로세스의 물리 메모리 상주량을 보는 지표이며, 공유 페이지도 포함합니다. PSS는 공유 페이지를 나누어 계산합니다. 파일 디스크립터는 파일·소켓·일부 이벤트 자원을 가리키는 운영체제의 번호입니다. 메모리가 안정적이어도 디스크립터가 계속 늘면 장기 실행에 문제가 생길 수 있습니다.

7-2. “스레드 상한 4”는 프로세스 전체 상한이 아니다

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 풀에 별도 상한을 적용합니다.

7-3. 네트워크와 지속성의 기본 경계

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을 포기한다고 명시합니다. 재배치 비용을 줄이려는 선택입니다. 이 설정 하나로 제품 전체의 안전성을 판정할 수는 없지만, 성능 최적화에는 다른 속성과의 교환이 있음을 보여 줍니다.

8직접 해볼 수 있는 실습 과제

정답을 예상하고, 관찰하고, 소스의 분기로 설명하기

아래는 독자가 별도로 수행하는 과제입니다. 코드 수정 과제는 개인 학습용 복사본에서 진행하고, 동작 실습은 설치가 준비된 환경과 직접 만든 시험 페이지를 사용합니다.

과제 1 · 입문

플래그 하나가 내부 정책이 되는 경로 그리기

목표: CLI 인자가 내부 설정으로 바뀌는 과정 이해.

  1. moli/src/cli.rs에서 layout 필드와 MOLI_LAYOUT을 찾습니다.
  2. moli/src/config.rs에서 bool 값이 LayoutPolicy로 바뀌는 분기를 찾습니다.
  3. moli-page-types/src/layout.rs의 기본값과 uses_real_layout()을 읽습니다.

완료 기준: “인자 → 설정 → 내부 enum → 허용되는 작업” 네 칸의 표를 작성합니다. Mock 모드에서 나온 좌표가 실제 CSS 배치의 증거인지도 설명합니다. 이 과제는 읽기만으로 끝낼 수 있습니다.

과제 2 · 입문

하나의 페이지를 두 가지 표현으로 읽기

목표: 사람이 읽는 Markdown과 구조 중심 시맨틱 트리의 차이 이해.

제목·링크·버튼·입력 필드가 있는 간단한 자기 페이지를 준비합니다. 아래 명령의 URL을 자신의 시험 페이지로 바꾸고 두 출력을 비교하세요. 예시 자체는 표준 예제 주소를 사용합니다.

moli fetch --dump markdown --wait-selector body https://example.com
moli fetch --dump semantic_tree_text --wait-selector body https://example.com

완료 기준: 각 출력에 남는 정보와 생략되는 정보를 세 가지씩 적습니다. 요약용 문서와 버튼 선택용 입력 중 어느 작업에 어떤 표현이 더 적합한지도 이유를 씁니다.

과제 3 · 중급

DOM 변경과 기하 정보의 시점을 비교하기

목표: 최신 DOM과 마지막 레이아웃이 다를 수 있음을 관찰.

  1. moli serve --layout에 CDP 클라이언트를 연결합니다.
  2. 너비 100px인 상자를 가진 자기 페이지를 열고 getBoundingClientRect()의 너비를 읽습니다.
  3. JavaScript로 상자의 CSS 너비를 200px로 바꾼 뒤, DOM의 style 값과 기하 정보를 각각 기록합니다.
  4. Page.captureScreenshot을 수행하고 다시 너비를 읽어 시점을 비교합니다.

완료 기준: 요청 순서·DOM 값·기하 값·예상 갱신 원인을 표로 정리합니다. 관련 시험이나 코드에서 추가 갱신 조건이 있는지도 확인합니다. 결과를 보기 전에 “모든 읽기가 재계산할 것”이라고 가정하지 마세요.

읽을 곳: ensure_initial_layout(), LatestLayoutTreeCache::publish(), with_fresh_layout_pass_in_environment(). 예상과 다르면 값보다 먼저 호출 순서를 검토합니다.

과제 4 · 중급

가짜 자원 표본으로 최신 커밋의 판단 재현하기

목표: 일시적인 후처리와 지속적인 누수를 구분.

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입니다. 기다리는 시간을 허용하는 것과 누수 허용치를 늘리는 것은 다릅니다.

과제 5 · 고급

공통 명령을 중심으로 작은 자동화 도구 설계하기

목표: 여러 입구에서 같은 실행 로직을 공유하는 구조 설계.

별도 학습 프로젝트에서 ReadTitle·ReadText 두 명령만 설계합니다. CLI 문자열 입력과 JSON 입력을 같은 내부 enum으로 변환하고, 실행 함수와 출력 어댑터를 분리합니다. 시작은 실제 브라우저 대신 메모리 문서 한 개여도 충분합니다.

완료 기준: 내부 실행 로직을 바꾸지 않고 두 입구가 같은 결과를 내야 합니다. 잘못된 명령의 오류도 각각의 외부 형식으로 변환되어야 합니다. 이어서 큐를 넣어 순서가 바뀌는 경우를 관찰해 보세요.

좋은 실습 결과물은 실행 성공 화면보다, 관찰한 결과를 소스의 조건문과 자료형으로 설명하는 기록입니다.

9관련 기술 심화 학습 로드맵

6주 동안 한 요청을 깊게 읽고, 작은 시스템으로 다시 만들기

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성능 측정 계획과 실패 기준. 두 입구를 가진 작은 자동화 프로젝트

9-1. 관심사별로 더 깊게 들어갈 갈림길

학습 전략

큰 공장의 모든 방을 먼저 외우기보다, 주문서 한 장이 들어와 제품 하나가 나갈 때까지 따라가 보세요. moli에서는 --dump screenshot이나 --dump markdown 하나가 그 주문서입니다.

9-2. 소스를 읽을 때 매번 남길 세 문장

  1. 입력: 이 함수는 무엇을 받으며, 호출 전에 어떤 조건이 필요할까?
  2. 소유와 수명: 무엇을 새로 만들고, 누가 보관하며, 언제 버릴까?
  3. 관찰: 오류·응답·이벤트 중 무엇으로 실행 결과를 확인할까?

이 세 문장이 명확하면 함수 이름을 많이 외우지 않아도 다음 파일을 고를 수 있습니다. 모르는 라이브러리는 공식 문서에서 그 자료형의 역할만 보충한 뒤 다시 호출 지점으로 돌아오는 방식이 좋습니다.

10핵심 키워드 사전

앞 절의 소스와 연결해서 기억할 단어들
용어쉬운 설명
헤드리스 브라우저사람이 보는 창 없이 웹 런타임을 실행하는 브라우저. 창이 없다고 JavaScript·배치·그림 기능이 모두 없는 것은 아닙니다.
브라우저 커널파싱·실행·네트워크·문서·렌더링을 조정하는 중심 시스템. moli는 Chromium 외부 프로세스를 감싼 방식과 구분됩니다.
DOM요소·텍스트·부모 자식 관계를 가진 문서 트리. JavaScript로 바뀔 수 있는 살아 있는 자료입니다.
계산 스타일CSS 우선순위와 상속 등을 적용해 요소에 정한 스타일. 실제 배치 치수와 같은 개념은 아닙니다.
레이아웃요소와 글자를 어느 위치에 얼마나 크게 놓을지 계산하는 단계.
페인트배치된 모양·글자·이미지를 시각 출력으로 그리는 단계.
Mock 기하 정보실제 CSS 배치 계산 대신 호환을 위해 일정한 규칙으로 정한 값. 실제 화면 좌표의 증거로 쓰면 안 됩니다.
FrozenLayoutTree한 번 계산한 기하 정보를 고정해 보관하는 트리. 변경 가능한 DOM 원본과 수명이 다릅니다.
V8 · IsolateV8은 JavaScript 실행 엔진, Isolate는 그 실행 상태를 분리하는 단위입니다. moli의 상태 소유·스케줄링과 함께 읽습니다.
CDPChrome DevTools 계열 자동화 명령·이벤트 규약. moli는 이 규약을 구현해 클라이언트의 연결을 받습니다.
WebDriver Classic세션과 HTTP 명령을 중심으로 브라우저를 조작하는 규약. Selenium 같은 도구와 연결됩니다.
WebDriver BiDi클라이언트와 브라우저가 양방향 메시지를 주고받는 WebDriver 규약. 명령뿐 아니라 이벤트 전달도 중요합니다.
어댑터외부 프로토콜 표현을 내부 명령으로 번역하고 결과를 다시 외부 형식으로 바꾸는 부품.
파사드많은 내부 모듈의 기능을 비교적 단순한 공개 입구로 제공하는 설계.
상태 기계가능한 상태와 그 사이의 이동을 정해 둔 모델. 파서의 스크립트 발견·중단·재개를 표현하는 데 적합합니다.
소유자 실행 영역특정 상태를 바꾸는 작업을 제어하는 실행 경계. 같은 상태를 아무 작업에서나 동시에 수정하지 않도록 합니다.
채널작업 사이에 명령·결과·깨우기 신호를 보내는 통로. 메시지를 보냈다는 사실과 처리가 끝났다는 사실은 다릅니다.
microtaskPromise 반응 등 JavaScript의 작은 후속 작업을 정해진 시점에 실행하는 체계. DOM 갱신 관찰 순서에 영향을 줍니다.
RAII · Drop자원의 수명을 값의 수명에 묶는 방식과 Rust의 정리 동작. 보통의 종료·오류 경로에서 후처리를 돕습니다.
차등 시험같은 입력을 두 구현에 넣어 결과를 비교하는 시험. 정규화 규칙과 관찰 시점이 결과의 의미를 정합니다.
WPT웹 플랫폼 동작을 시험하는 공개 시험 모음. 선택한 범위와 통과 건수를 함께 봐야 합니다.
RSS · PSS프로세스 메모리를 보는 서로 다른 지표. 공유 메모리 계산 방식이 달라 숫자를 바로 섞어 비교하면 안 됩니다.
FD · eventpoll · eventfd운영체제의 파일·소켓·이벤트 자원과 관련된 관측값. Linux 자원 회수 시험에서 증가량을 감시합니다.
Cargo workspace여러 Rust 패키지의 의존성·빌드·정책을 한 저장소에서 관리하는 단위. 이 레포는 91개 멤버로 구성됩니다.
lockfile · patchlockfile은 선택한 의존성 버전을 기록합니다. patch는 같은 의존성 이름이 가리키는 소스를 포크나 로컬 코드로 바꿀 수 있습니다.
shallow clone커밋 이력을 일부만 가져온 복제본. 현재 파일 분석은 가능하지만 전체 역사·기여량 통계에는 부족합니다.
WebMCP사이트가 제공하는 도구를 에이전트가 발견하고 호출하는 방향의 인터페이스. 이 레포에는 관련 런타임과 playground가 있습니다.

단어를 외우기보다 이 글의 예와 짝지어 보세요. 스냅샷은 LatestLayoutTreeCache, 어댑터는 AutomationCommand로의 변환, 상태 기계는 RawParserStep처럼 기억하면 됩니다.

11참고 링크

버전이 고정된 소스와, 추가 학습을 위한 공식 문서

11-1. 공식 저장소·프로젝트 자료

11-2. 본문에서 따라간 핵심 소스

아래 링크는 모두 커밋 95094dd 기준입니다. 같은 버전의 코드와 설정을 함께 살펴볼 수 있습니다.

11-3. 관련 기술의 공식 학습 자료

현재 상태 (2026-10-23 확인)

항목값확인 근거·범위
선정TrendShift Monthly #92026-10-23 TrendShift 기준
스타 / 포크20,184 / 5422026-10-23 GitHub 기준
생성일2026-08-102026-10-23 GitHub 기준
주 언어Rust2026-10-23 GitHub 기준
최신 릴리스v1.1.19 · 2026-10-212026-10-23 GitHub 기준. moli/Cargo.toml의 버전도 1.1.19
최근 push2026-10-232026-10-23 GitHub 기준
분석 HEAD95094dd · 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절
workspace91개 멤버루트 Cargo.toml의 명시적 목록
라이선스Apache-2.02026-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/, 파일별 고지, 외부 코드·시험 자료의 예외도 존재합니다.