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

pdfcraft 딥다이브
— PDF 한 장을 고치며 배우는 Rust 앱의 속사정

PDF 편집기

빠른 판단

무엇
PDF 보기·페이지 정리·주석·양식·암호화 엔진을 GUI·웹·CLI·MCP에서 공유하는 Rust PDF 편집기
이럴 때
로컬 PDF를 읽고 병합·분할·편집하거나 같은 작업을 명령·에이전트로 반복 실행할 때
입력
PDF 문서와 페이지·주석·양식 편집 명령
결과
편집·병합·분할된 PDF와 처리 결과
화면의 회전 버튼 하나가 문서 객체, 실행 취소 기록, 렌더링 작업, 디스크 저장으로 이어집니다. PdfCraft는 그 연결을 GUI·CLI·MCP에서 함께 사용하는 Rust PDF 작업대입니다. 이 글은 커밋 0dc77fe의 구현을 따라가며, 초보 개발자가 큰 프로그램을 읽는 순서와 안전한 편집기를 만드는 방법을 익힙니다. 2026-10-23 GitHub 기준 스타 17,301 · 포크 6,644 · 최신 릴리스 v0.13.0.
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

PDF를 보여주는 화면 뒤에 문서 엔진과 자동화 창구가 함께 있습니다

PdfCraft는 Rust로 만든 오픈소스 PDF 읽기·편집 앱입니다. 페이지 보기와 검색, 회전·삭제·병합·분할, 주석, 양식, 암호화 같은 작업을 다룹니다. 데스크톱 앱뿐 아니라 명령줄 도구와 WebAssembly 웹 앱도 같은 저장소에 들어 있습니다.

이 저장소의 핵심은 PDF 기능의 개수보다, 하나의 편집 엔진을 여러 사용 방식에서 재사용하는 구조입니다. 사람이 메뉴를 누르든 자동화 도구가 JSON을 보내든, 문서를 바꾸는 핵심 처리는 crates/engine에 모입니다.

세 진입점은 apps/pdfcraft, apps/pdfcraft-cli, apps/pdfcraft-web입니다. 사용자 인터페이스는 crates/ui-egui, 자동화는 crates/automation이 담당합니다. 웹 서비스를 만들 때 익힌 프론트엔드·백엔드 구분을 그대로 적용하면 헷갈립니다. 여기서 엔진은 주로 사용자 기기 안의 라이브러리로 실행됩니다.

용어 먼저
PDF는 그림 파일이 아니라 객체들의 연결망입니다
페이지, 글꼴, 이미지, 주석을 각각의 객체로 저장하고 번호로 참조합니다. crates/cos/src/object.rs의 ObjRef는 객체 번호 num과 세대 번호 generation을 가집니다. 페이지를 고친다는 것은 이 연결망의 일부를 바꾸는 일입니다.
앱 전체를 이해하는 비유

같은 문서 수선소에 접수 창구가 세 개 있습니다

GUI는 사람이 찾아오는 창구, CLI는 작업 지시서를 받는 창구, MCP는 AI 도구가 사용하는 창구입니다. 수선공인 엔진과 문서 재료 창고는 공유합니다. 창구마다 다른 수선 규칙을 만들면 같은 파일이 서로 다르게 고쳐지므로, 공통 규칙을 안쪽에 둡니다.

실행 취소는 수선 전 상태를 보관하는 장부, 저장은 검수한 결과를 파일로 납품하는 과정입니다. 화면이 예뻐도 장부와 납품 절차가 틀리면 문서를 잃을 수 있습니다.

초보자가 따라갈 첫 번째 기능: 페이지 회전

  1. 자동화 도구 page_rotate가 사용자 기준 페이지 번호와 각도를 받습니다.
  2. Automation::call()이 인자를 확인하고 Edit::RotatePages로 연결합니다.
  3. Session::apply()가 작업용 문서에서 수정하고 실행 취소 상태를 남깁니다.
  4. organize::rotate_pages()가 페이지 사전의 /Rotate를 바꿉니다.
  5. 변경 내용을 바이트로 만들어 화면을 갱신하고, 별도의 저장 요청 때 파일에 기록합니다.

따라서 “90도 회전”은 화면 이미지를 돌리는 효과만을 뜻하지 않습니다. PDF의 회전 속성을 수정하는 편집과 현재 보기를 돌리는 동작은 구분해야 합니다. 이 구분이 문서 모델과 화면 상태를 나누는 출발점입니다.

읽기 목표
PDF 표준 전체를 외울 필요는 없습니다

먼저 “입력 → 검증 → 수정 → 기록 → 표시 → 저장”을 설명할 수 있으면 됩니다. 이후 각 단계에서 Rust의 소유권, 오류 처리, 자료구조, 스레드가 왜 필요한지 하나씩 연결해 보세요.

2왜 주목받는가

관심 지표와 코드에서 읽을 수 있는 매력, 그리고 검증 범위를 나눠 봅니다

TrendShift Monthly #14 선정 대상이며, 2026-10-23 GitHub 기준 스타 17,301, 포크 6,644입니다. 저장소 생성일은 2026-09-30으로 분석일까지 23일입니다. 매우 짧은 공개 이력에 관심이 모였다는 사실은 읽을 수 있지만, 생성일을 개발 시작일로 보거나 스타를 품질 점수로 바꾸어 해석할 수는 없습니다.

주목 요인으로 해석할 수 있는 부분은 세 가지입니다. 첫째, 익숙한 PDF 편집 문제를 Rust로 풀어 내부를 공개합니다. 둘째, 데스크톱·웹·자동화를 하나의 코드 기반으로 묶습니다. 셋째, 실행 취소·증분 저장·파일 교체처럼 눈에 잘 안 보이는 신뢰성 문제를 구현과 테스트로 드러냅니다. 이는 기능과 구조에 근거한 해석이며, 이용자 설문이나 유입 경로로 확인한 인기 원인은 아닙니다.

다른 선택지와 비교할 때 볼 지점

비교 대상선택할 때의 중심 질문PdfCraft에서 눈에 띄는 차이
열람 중심 PDF 도구 문서를 편하게 읽고 필요한 내용을 찾는가? 열람에서 더 나아가 객체 수정·실행 취소·저장까지 같은 엔진에서 추적할 수 있습니다.
PDF 처리 라이브러리 내 프로그램에 필요한 PDF 기능을 어떻게 붙일까? 라이브러리 위에 GUI·CLI·MCP가 연결된 완성형 앱의 구성까지 배울 수 있습니다.
업로드형 변환 서비스 파일을 서버에 보내 처리하는 흐름이 적합한가? 로컬 엔진과 정적 웹 배포 구조가 있어, 서버 업로드 없이 처리하는 앱을 설계할 출발점이 됩니다.
업무용 상용 편집기 내 문서·프린터·서명 업무가 충분히 검증되어 있는가? 소스를 고치고 자동화 경로를 이해할 수 있는 것이 장점입니다. 업무 호환성은 별도 비교가 필요합니다.

이 표는 제품별 모든 기능이나 속도를 판정하는 벤치마크가 아닙니다. PdfCraft의 실질적 장점은 처리 흐름을 관찰하고 바꿀 수 있는 범위입니다. 특히 PDF 라이브러리와 앱 사이에 어떤 상태·검증·운영 코드가 필요한지 보여 줍니다.

“순수 Rust로 처음부터”라는 소개를 정확히 읽기

GitHub 설명과 README는 Rust 구현을 강조합니다. 실제 앱·엔진은 Rust 중심이지만, crates/render/src/lib.rs는 현재 페이지 렌더링에 hayro, 검사·호환성 보완에 lopdf를 사용하는 초기 구현 단계라고 밝힙니다. 루트 Cargo.toml의 [patch.crates-io]와 vendor/도 이를 뒷받침합니다.

“Rust로 작성했다”와 “PDF 처리의 모든 부품을 독자 구현했다”는 서로 다른 주장입니다. 이 차이를 읽는 것이 오픈소스를 평가하는 좋은 훈련입니다. 의존성을 숨기는 대신 교체 경계를 둔 구조 자체도 배울 만합니다.

관심도와 완성도는 따로
README도 현재 상태를 알파로 설명합니다

기존 텍스트 편집, 라틴 문자 밖의 OCR, 오피스 형식 변환, 전문 인쇄·장기 서명 검증 등에 한계가 있다고 명시합니다. README의 983개 PDF 코퍼스 중 963개 열기·렌더링 성공, 958개 편집 후 저장 성공, 충돌 0건이라는 수치는 프로젝트가 보고한 결과입니다. 다른 파일이나 적대적 입력 전체의 무결성을 보장하지 않습니다.

3기술 스택 전체 지도

Cargo 선언은 허용 범위, Cargo.lock은 이 커밋이 선택한 실제 버전입니다

루트 Cargo.toml은 resolver = "3", edition = "2024", 워크스페이스 버전 0.13.0, rust-version = "1.95"를 선언합니다. 실제 하위 패키지는 crates/ 33개, apps/ 3개, xtask 1개로 총 37개입니다. vendor는 워크스페이스 멤버에서 제외되지만 패치 의존성으로 사용됩니다.

이 프로젝트의 “백엔드”는 HTTP 서버보다 문서 처리 엔진에 가깝습니다. 별도 데이터베이스나 서버 애플리케이션을 필수로 두지 않습니다. 프론트엔드도 React·Tauri·Electron이 아니라 Rust의 egui 계열입니다.

① 엔진·문서 처리

기술선언 → 잠금 버전실제 역할과 근거
Rust / Cargo Rust 1.95 기준 · Edition 2024 루트 Cargo.toml의 공통 설정. README도 빌드에 Rust 1.95 이상이 필요하다고 안내합니다.
hayro / lopdf 0.7.1 → 0.7.1
0.45 → 0.45.0
crates/render/Cargo.toml. 페이지 래스터화와 문서 검사·복구 경로에 사용합니다.
hayro-syntax 0.7.2 → 0.7.2 렌더링 계열의 PDF 해석 부품. 수정판을 vendor/hayro-syntax에서 가져옵니다.
serde / serde_json 1 → 1.0.229
1 → 1.0.151
구조체 저장·복원과 자동화 JSON 입출력. 루트에서 공유합니다.
thiserror / chrono 2 → 2.0.21
0.4.45 → 0.4.45
타입 있는 오류와 날짜 변환. 저장 시 수정 시각도 chrono를 통해 다룹니다.
flate2 / weezl 1.1 / 0.2 선언 PDF 압축 필터에 필요한 부품. flate2는 기본 기능을 끄고 rust_backend를 선택합니다.
ocrs / rten 0.13.1 → 0.13.1
0.26.0 → 0.26.0
crates/ocr/Cargo.toml. 이미지의 글자를 인식하고 검색용 텍스트 층을 만드는 경로입니다.
boa_engine 0.22.0 → 0.22.0 crates/js/Cargo.toml. PDF 양식의 JavaScript 일부를 Rust 인터프리터에서 처리합니다.
skrifa / harfrust 0.44 / 0.12 선언 crates/fonts/Cargo.toml. 글꼴 윤곽과 아랍 문자 조형 등에 사용합니다. skrifa 잠금 버전은 이 경로에서 0.44.0입니다.

Cargo.lock에는 skrifa 0.42.1·0.44.0·0.47.0처럼 같은 이름의 여러 버전도 있습니다. 따라서 잠금 파일에서 이름 하나만 검색해 “앱의 버전”이라고 단정하면 안 됩니다. 먼저 해당 크레이트의 의존성 선언을 찾으세요.

의존성 파일 읽는 법
워크스페이스와 기능 플래그
워크스페이스는 관련 패키지를 묶어 공통 버전·검사를 관리하는 단위입니다. workspace = true는 루트 선언을 물려받습니다. features는 선택 기능을 켜는 장치이며, default-features = false는 라이브러리가 기본으로 켜는 기능 묶음을 끕니다.

② 화면·운영체제 연결

구성버전 / 설정무엇을 배울 수 있나
egui / eframe 0.36 → 0.36.2 Rust 코드로 UI를 구성하고 데스크톱과 웹에서 공유하는 방법.
데스크톱 표시 wgpu + glow 기능 apps/pdfcraft/Cargo.toml은 GPU 표시와 OpenGL 대체 경로, Wayland·X11, 접근성·설정 저장 기능을 켭니다.
파일 선택 rfd 0.17 → 0.17.2 운영체제의 파일 대화상자와 플랫폼별 코드를 엔진 바깥에 두는 방법.
웹 앱 wasm-bindgen-futures 0.4
web-sys / js-sys 0.3 선언
apps/pdfcraft-web/Cargo.toml. 브라우저 API·비동기 다운로드를 Rust와 연결합니다.
공유 UI 부품 craft-ui 0.1.0 + Git rev 루트에서 커밋 5c874d51…로 고정합니다. 버전 문자열 외에 Git 커밋도 재현성의 일부입니다.

PDF를 픽셀로 만드는 단계와 그 픽셀·버튼을 창에 그리는 단계는 다릅니다. crates/render/src/lib.rs는 hayro-interpret와 vello_cpu 경로를 설명합니다. wgpu가 의존성에 있다고 PDF 해석 전체가 GPU에서 실행된다고 읽으면 안 됩니다.

③ 인프라·빌드·검증

파일확인한 설정의미
Cargo.toml unsafe_code = "forbid"
릴리스 lto = "thin", codegen-units = 1
자체 워크스페이스의 unsafe 제한과 릴리스 최적화. 외부 의존성까지 unsafe가 전혀 없다는 선언은 아닙니다.
apps/pdfcraft-web/Trunk.toml public_url = "./", filehash = true
개발 서버 127.0.0.1:8766
apps/pdfcraft-web/ 설정. 하위 경로 배포와 해시 기반 캐시에 대응합니다.
.github/workflows/ci.yml Ubuntu·Windows 검사, 별도 wasm 검사 포맷·Clippy·테스트·계층·자산 검사를 수행합니다. macOS는 이 행렬에서 제외되어 있습니다.
xtask/ layers, assets, wasm, fuzz 빌드 보조 작업도 Rust 명령으로 묶어 개발 환경과 CI에서 공유합니다.
.github/workflows/release.yml 공유 워크플로 release.yml@main 호출 릴리스 브랜치 push 또는 수동 실행. 외부 워크플로가 움직이므로 앱 커밋만으로 배포 절차 전체가 고정되지는 않습니다.

스택을 읽을 때는 라이브러리 이름보다, 어느 계층이 어떤 기능을 켜서 사용하는지를 함께 보아야 합니다. 예를 들어 CLI의 MCP는 기본 빌드 기능에 포함되어도, 서버 실행은 pdfcraft-cli mcp를 명시적으로 호출해야 시작됩니다.

4아키텍처 심화 분석

한 번의 편집을 따라가며 네 가지 설계 패턴을 읽습니다

전체 시스템 구조

사용자 입력 / 자동화 요청 | +-- 데스크톱: apps/pdfcraft --> ui-egui ---------+ | | +-- 브라우저: apps/pdfcraft-web --> ui-egui ------+--> engine::Session | | | +-- CLI run / MCP --> automation::Automation ----+ +-- Edit / undo / redo +-- 문서별 상태 +-- 저장 바이트 생성 | +-------------------------------------------+ | +----------------+-------------------+ | | v v organize / edit / forms / annot ... render | / \ v hayro cos + lopdf 검사 model / cos +--> 텍스트 추출 | vello_cpu v | 객체 그래프 + 변경 overlay 페이지 픽셀 | filters / crypt / geom 저장: engine 바이트 --> check_save --> platform의 임시 파일·교체 --> mark_saved 표시: 페이지 픽셀 --> ui-egui --> 데스크톱 wgpu/glow 또는 웹 Glow 설명용으로 주요 연결만 표시했습니다. 세부 의존성 전체를 뜻하지 않습니다.

계층 번호의 근거는 xtask/src/layers.rs의 TABLE입니다. 기초 도구 L0, 객체 계층 L1, 모델 L2, 렌더링·양식 L3, 편집 기능 L4, 엔진 L6, UI·플랫폼·자동화 L7로 구분합니다. 표에는 미래용 이름도 있으므로, 이름이 등록되었다고 해당 크레이트가 실제 존재하는 것은 아닙니다.

UI_MIN_LAYER = 7은 낮은 계층에 UI 도구가 들어오는 일을 막는 기준입니다. cos의 내부 의존성 허용 목록도 filters·crypt·geom으로 제한합니다. 설계 문구를 넘어서 검사 가능한 규칙으로 만든 점이 중요합니다.

패턴 1. 파사드와 명령 목록: 같은 규칙으로 들어오는 여러 문

파사드는 복잡한 내부 기능 앞에 두는 공통 출입구입니다. crates/engine/src/lib.rs의 Session은 열린 문서와 편집 기록을 관리합니다. GUI의 PdfCraftApp::apply_edit()는 self.session.apply(id, edit.clone())를 호출합니다. 자동화도 동일한 Session을 소유합니다.

crates/engine/src/commands.rs의 CommandSpec·Needs는 명령의 이름·단축키·실행 조건을 표현합니다. Needs::Undo처럼 실행 가능 조건을 데이터로 두면 메뉴와 자동화가 공통 기준을 참고할 수 있습니다. 모든 UI 상태가 엔진에 들어가지는 않습니다. 확대율·다음 검색 결과처럼 보기 전용 동작은 화면 상태에 남습니다.

자동화 쪽의 crates/automation/src/tools.rs는 별도의 도구 정의표입니다. ToolDef에는 input_schema, read_only, destructive, 대응 명령 command가 있습니다. 명령 목록과 도구 목록은 연결되지만 동일한 자료구조는 아닙니다.

// crates/automation/src/lib.rs의 Automation::call() 일부
let def = tools::find(name).ok_or_else(|| ToolError::UnknownTool(name.into()))?;
tools::check_args(def, args)?;
// 이후 도구별 분기에서 엔진 기능을 호출합니다.

schema()는 additionalProperties: false를 넣습니다. 인자 오타를 무시하지 않고 오류로 처리하겠다는 계약입니다. crates/automation/src/mcp.rs의 McpServer::serve()는 표준입력에서 한 줄씩 읽어 JSON-RPC로 해석하고 표준출력으로 응답합니다. HTTP 포트를 열지 않습니다.

다른 프로젝트에 적용하면

할 일 앱에 GUI와 자동화가 함께 있다면, “완료 처리”를 버튼 함수 안에만 쓰지 마세요. 공통 서비스에 규칙을 두고 버튼·명령줄·도구 호출이 그 서비스를 사용하게 만들면 됩니다. 화면에 표시할 이름과 실제 실행 코드를 분리하는 연습도 함께 할 수 있습니다.

패턴 2. 작업 사본과 상태 스냅샷: 성공한 편집만 채택하기

cos::Document는 원본 바이트를 Arc<Vec<u8>>로 공유하고, 바뀐 객체를 overlay: BTreeMap<u32, Slot>에 따로 보관합니다. 변경하지 않은 큰 데이터는 공유하고 변경분만 따로 갖는 방식이 복사 시 쓰기입니다.

Session::apply()는 먼저 editor.cos.clone()으로 작업 사본을 만든 뒤 run_edit()를 실행합니다. Document::clone()이 항상 비용 0인 것은 아닙니다. 바이트와 객체 등은 공유하지만 overlay 맵과 일부 메타데이터의 복사 비용은 남습니다.

// crates/engine/src/lib.rs, apply()의 서로 떨어진 핵심 단계
let mut next = editor.cos.clone();
// run_edit(&mut next, ...)가 성공한 뒤 이전 상태와 교환합니다.
let previous = std::mem::replace(&mut editor.cos, next);
// 이전 상태와 암호 키·갱신 범위를 실행 취소 기록에 함께 넣습니다.
editor.undo.push((label, previous, previous_keys, scope));

문서별 실행 취소 상한은 MAX_UNDO = 100입니다. 새 편집이 성공하면 redo 목록을 비웁니다. undo()와 redo()는 현재 상태와 보관 상태를 교환합니다. 단순히 “반대 명령”을 계산하지 않으므로, 복잡한 편집도 상태로 되돌릴 수 있습니다.

갱신한 문서를 표시할 수 없으면 apply()는 이전 상태를 복구하고 오류를 반환합니다. 다만 이것을 데이터베이스의 완전한 트랜잭션이나 외부 부작용 전체의 원자성으로 확대해 읽으면 안 됩니다. 이 경계가 보호하는 대상은 엔진의 문서 편집 상태입니다.

좋은 반례도 있습니다. crates/organize/src/lib.rs의 delete_pages() 주석은 함수 자체가 트랜잭션을 보장하지 않으며 실패 시 일부 변경이 남을 수 있다고 밝힙니다. 그래서 상위 엔진이 작업 사본을 사용하는 것입니다. 안전한 동작은 모든 함수가 아니라 적절한 경계에서 완성됩니다.

패턴 3. 증분 저장과 원자적 교체: 내용과 파일을 따로 보호하기

crates/cos/src/writer.rs의 write_incremental()은 원본 뒤에 수정 객체와 새 교차 참조 정보를 붙입니다. 새 trailer의 /Prev는 이전 교차 참조 구역을 가리킵니다. 기존 객체의 모든 속성을 새 스키마로 재생성하지 않아 모르는 데이터를 보존하는 데도 유리합니다.

보통의 증분 저장 [원본 객체][이전 xref][이전 EOF] + [수정 객체][새 xref][새 EOF] | +-- /Prev --> 이전 xref 파일을 바꾸는 절차 저장 바이트 생성 --> 다시 열어 검사 --> 같은 폴더에 임시 파일 생성 --> 쓰기·sync_all --> 대상 파일로 이름 교체 --> 저장 완료 기록

하지만 write_incremental()의 맨 앞에는 예외가 있습니다. reconstructed(), encryption_changed(), full_save_required() 중 하나가 참이면 write_full()로 넘어갑니다. 손상 파일 복구, 암호화 변경, 민감 내용 삭제에는 전체 재작성이 필요할 수 있습니다.

// crates/cos/src/writer.rs의 실제 분기
if doc.reconstructed() || doc.encryption_changed() || doc.full_save_required() {
    return write_full(doc, opts);
}

여기서 가림 표시와 실제 삭제를 구별할 수 있습니다. 민감한 글자 위에 검은 사각형을 그려도 원본 내용은 남을 수 있습니다. 삭제를 적용한 뒤 예전 revision을 파일에 남기는 것도 위험하므로, 전체 재작성 여부가 보안 의미를 가집니다.

Automation::doc_save_with()는 같은 파일이면 기본적으로 증분 저장, 새 경로면 기본적으로 전체 저장을 선택합니다. 이어 publish_save()가 Session::check_save() → 파일 쓰기 → mark_saved() 순서를 지킵니다. “검사했지만 저장은 실패한 상태”와 “저장 완료 상태”를 섞지 않는 순서입니다.

파일 교체는 crates/platform/src/staging.rs의 write_atomically()가 담당합니다. 대상 옆에 create_new(true)로 임시 파일을 만들고, 내용을 기록·동기화한 뒤 이름을 바꿉니다. 이미 존재하는 임시 이름을 열어 덮어쓰지 않습니다. 파일 내용의 동기화는 수행하지만, 정전 때 부모 디렉터리의 이름 변경까지 영속화된다는 보장은 주석에 명시적으로 제외되어 있습니다.

증분 저장은 문서 내용을 어떻게 구성할지의 문제이고, 원자적 저장은 완성된 파일을 어떻게 교체할지의 문제입니다. 서로 다른 실패를 막으므로 두 가지가 함께 필요합니다.

패턴 4. 제한된 작업 큐: 지금 필요한 화면을 먼저 그리기

큰 PDF에서 모든 페이지를 최고 해상도로 동시에 그리면 메모리와 응답성이 무너집니다. crates/render/src/raster.rs의 RenderPool은 렌더 요청을 처리하고, RenderRequest는 페이지·배율·타일·종류·태그를 담습니다. RequestKind는 픽셀 요청과 텍스트 요청을 구분합니다.

crates/render/src/raster/work_queue.rs의 WorkQueue::replace()는 새로 필요한 요청 목록으로 교체하고 중복을 제거합니다. 필요 없어진 완료 결과도 버립니다. State::take()는 완료 순서만 따르지 않고 현재 요청 우선순위를 다시 봅니다. 페이지를 급히 넘겼을 때 과거 화면이 현재 화면보다 먼저 나오는 일을 줄이는 설계입니다.

완료 결과 보관 기준은 RESULT_BYTES = 32 * 1024 * 1024, RESULT_COUNT = 32입니다. pop()은 한도에 도달하면 새 작업을 꺼내지 않습니다. 다만 진행 중인 렌더링은 이 보관 예산 바깥이고, 큰 완료 결과 하나를 단독으로 허용하는 예외도 있습니다. 이 수치를 프로세스 전체 메모리 제한으로 읽으면 안 됩니다.

실무에서 자주 만나는 개념
배압과 오래된 결과 처리
처리 속도보다 결과 생산 속도가 빠를 때 생산을 늦추는 것이 배압입니다. 이미 실행 중인 작업은 즉시 멈추지 못할 수 있으므로, 더 이상 필요 없는 결과를 채택하지 않는 규칙도 함께 필요합니다. 검색 자동완성·지도 타일·이미지 편집 미리보기에도 같은 문제가 있습니다.

5디렉토리 구조 해부

폴더 이름을 외우기보다 읽을 경로를 정합니다
pdfcraft/ ├── Cargo.toml / Cargo.lock 공통 설정과 선택된 의존성 ├── apps/ │ ├── pdfcraft/ 데스크톱 시작·설정·업데이트 │ ├── pdfcraft-cli/ 명령 해석·스크립트 실행 │ └── pdfcraft-web/ WASM 진입점·Trunk 설정 ├── crates/ │ ├── geom/ filters/ crypt/ 좌표·압축·암호화 기초 │ ├── cos/ model/ content/ 객체·페이지 모델·내용 해석 │ ├── fonts/ 글꼴 측정·인코딩·조형 │ ├── render/ 페이지 픽셀·텍스트·검사 │ ├── organize/ edit/ 페이지 구조·기존 내용 편집 │ ├── annot/ forms/ js/ xfa/ 주석·양식·양식 스크립트 │ ├── redact/ sign/ audit/ 민감 내용 삭제·서명·감사 │ ├── create/ export/ optimize/ 생성·내보내기·최적화 │ ├── ocr/ compare/ measure/ 문자 인식·비교·측정 │ ├── a11y/ preflight/ print/ 접근성·검사·인쇄 │ ├── xfdf/ 양식·주석 교환 형식 │ ├── engine/ 공통 편집 경계·문서 상태 │ ├── platform/ 운영체제 공통 서비스 │ ├── automation/ JSON 도구·MCP │ ├── ui-egui/ 화면·입력·번역 카탈로그 │ └── oracle/ testkit/ 외부 검증 도구·테스트 지원 ├── vendor/ 수정한 외부 크레이트 ├── xtask/ 계층·자산·코퍼스·빌드 검사 ├── packaging/ OS별·웹 배포 절차 ├── parity/ 기능 대응 현황과 근거 ├── docs/ 설계·사용·한계 문서 ├── assets/ 앱 자산·글꼴·모델 관련 자료 ├── ATTRIBUTION.toml 자산 출처·라이선스·해시 └── .github/workflows/ CI와 릴리스 연결

위 트리의 crates/ 묶음은 실제 존재하는 33개 패키지입니다. 내부 파일까지 펼치지 않아도 “화면”, “엔진”, “문서 부품”, “검증·배포”가 갈린다는 점을 먼저 찾을 수 있습니다.

역할별로 처음 열 파일

알고 싶은 것파일과 탐색 지점읽을 때 던질 질문
공통 편집 규칙crates/engine/src/lib.rs
Session::apply()
어떤 단계까지 성공해야 문서가 바뀌었다고 인정하는가?
PDF 객체 표현crates/cos/src/object.rs
ObjRef, Object, Dict
객체를 저장한 값과 객체를 가리키는 참조는 어떻게 다른가?
페이지 회전crates/organize/src/lib.rs
rotate_pages()
각도를 검증하고 정규화하는 순서는 무엇인가?
화면과 엔진 연결crates/ui-egui/src/editing.rs
apply_edit()
성공·실패 뒤 화면 상태를 어떻게 맞추는가?
자동화 입력 계약crates/automation/src/tools.rs
tools(), check_args()
문서·페이지·좌표의 단위는 어디에서 정해지는가?
실패 회귀 테스트crates/automation/src/lib.rs
invalid_save_bytes_never_reach_publication_or_mark_saved()
오류 반환 외에 기존 파일과 문서 상태도 확인하는가?

줄 수가 큰 파일은 처음부터 끝까지 읽지 않아도 됩니다. 식별자를 검색해 정의·호출·테스트를 오가세요. apply처럼 흔한 이름은 경로를 좁힌 뒤 찾고, Edit::RotatePages처럼 고유한 이름은 저장소 전체에서 찾는 편이 효율적입니다.

# 저장소 루트에서 읽기만 하는 탐색 명령
rg -n 'pub fn apply|pub fn undo|pub fn save_bytes' crates/engine/src/lib.rs
rg -n 'RotatePages|page_rotate|fn rotate_pages' crates apps
rg -n 'check_save|mark_saved|write_atomically' crates/automation crates/platform
rg -n 'RESULT_BYTES|fn replace|fn take' crates/render/src/raster/work_queue.rs

코드 규모: 크다고 모두 앱의 독자 구현은 아닙니다

범위 / 언어파일 수물리적 줄 수
vendor 제외 Rust509305,508
vendor 내부 Rust443173,789
Rust 합계952479,297
vendor 제외 Shell9805
vendor 제외 PowerShell4620
vendor 제외 Python1158
vendor 내부 JavaScript211
vendor 제외 HTML2825

집계 기준은 커밋 0dc77fe의 확장자별 텍스트 줄 수이며 .git/는 제외합니다. 테스트·생성 데이터·주석·빈 줄을 포함하고, Markdown 문서는 위 코드 표에서 제외했습니다. TOML 15,087줄, YAML 계열 570줄도 vendor 바깥에서 확인되지만, 구현 언어와 설정을 섞지 않기 위해 따로 봅니다.

특히 crates/ui-egui의 Rust는 102,466줄, crates/engine은 23,505줄, crates/automation은 19,910줄입니다. 이 수치도 테스트를 포함한 폴더별 합계입니다. 작은 핵심 흐름부터 읽는 편이 30만 줄을 순서대로 읽는 것보다 이해에 도움이 됩니다.

6학습 포인트 (기술별)

문법을 배운 뒤, 그 문법이 해결하는 실제 문제를 찾습니다

① Rust 소유권: 큰 문서의 사본은 어떻게 관리할까?

Arc<T>는 여러 소유자가 같은 값을 공유하도록 하는 참조 카운트 포인터입니다. 여기서는 원본 PDF 바이트와 이미 읽은 객체를 여러 상태가 함께 보유합니다. 마지막 소유자가 사라질 때 데이터가 해제됩니다. Arc를 복제하는 것과 내부 Vec 전체를 복제하는 것을 구별해야 합니다.

읽을 파일은 crates/cos/src/document.rs입니다. Document의 data·cache·overlay를 나란히 보세요. 원본 공유, 파싱 결과 재사용, 편집 변경분 저장은 서로 다른 목적입니다. Mutex는 공유 가변 데이터에 접근할 순서를 조정합니다.

헷갈리기 쉬운 차이
공유 소유권은 자동 메모리 제한이 아닙니다
Arc는 불필요한 전체 복사를 줄이지만, 오래된 스냅샷이 원본을 붙잡으면 데이터는 계속 살아 있습니다. 실행 취소 개수, 캐시 예산, 결과 큐 제한을 따로 두는 이유입니다. Weak는 데이터를 계속 살려 두지 않는 약한 참조입니다.

작은 실습: 종이에 원본 바이트 하나, 편집 상태 둘, undo 상태 하나를 그려 연결하세요. 상태를 하나 지웠을 때 원본이 해제되는지 설명해 보세요. 이후 crates/automation/src/text_cache.rs가 문서 버전에 Weak<Vec<u8>>를 쓰는 이유를 찾아보세요.

② 오류 처리: 실패해도 문서가 남게 하기

crates/render/src/lib.rs의 OpenError는 읽기 불가·암호 필요·암호 오류·지원 불가를 나눕니다. 자동화의 ToolError도 알 수 없는 도구, 잘못된 인자, 실행 실패를 구분합니다. 호출자가 다음 행동을 선택하려면 실패 종류가 표현되어야 합니다.

?는 오류를 호출자에게 전달합니다. 편집 경계의 guard()는 catch_unwind를 사용해 예기치 않은 panic을 오류로 바꾸는 마지막 방어선입니다. 무한 반복이나 프로세스 강제 종료까지 해결하는 장치는 아니므로, 입력 예산과 작업 감시도 따로 필요합니다.

작은 실습: page_rotate에 45도를 넣는 경우를 따라가세요. 자동화 계층은 degrees % 90을 검사하고, 낮은 계층의 rotate_pages()도 다시 검사합니다. 서로 다른 API 진입 경로에서도 계약을 지키기 위한 중복 검증입니다.

③ PDF 자료구조: 페이지는 왜 트리인가?

crates/model/src/lib.rs의 PageTree는 Iterator로 페이지를 방문합니다. seen: HashSet<ObjRef>는 같은 객체를 반복 방문하지 않게 합니다. 손상된 문서는 순환 참조를 가질 수 있어, 정상 트리라는 가정만으로 재귀를 쓰면 위험합니다.

상속 가능한 속성은 Resources·MediaBox·CropBox·Rotate 네 가지입니다. 부모가 선언한 값을 자식 페이지가 물려받습니다. 페이지 순서를 재구성할 때는 유효한 상속값을 보존해야 하므로 단순한 배열 재정렬보다 일이 많습니다.

작은 실습: 부모에 /Rotate 90, 첫째 자식에 회전값 없음, 둘째 자식에 /Rotate 180을 둔 트리를 그립니다. 두 페이지의 유효 회전이 무엇인지 적고, 부모를 없애도 같은 결과가 되려면 무엇을 자식에 복사해야 하는지 설명해 보세요.

④ 좌표와 변환: 화면의 왼쪽 위가 PDF의 원점일까?

자동화 API는 표시된 페이지의 왼쪽 위를 원점으로, 아래쪽을 y축 양의 방향으로 삼아 PDF 포인트 좌표를 받습니다. 반면 model::Page::view_matrix()와 view_matrix_for()는 왼쪽 아래가 원점이고 위쪽이 y축 양의 방향인 표시 좌표를 PDF 사용자 공간으로 변환합니다. 따라서 자동화 좌표에 이 행렬을 적용하려면 y축 방향 차이도 반영해야 합니다. 두 함수 중 앞의 함수는 페이지의 회전·표시 영역·UserUnit을, 뒤의 함수는 인자로 받은 회전·영역을 반영합니다.

// crates/model/src/lib.rs, view_matrix_for()의 90도 분기
90 => [0.0, 1.0, -1.0, 0.0, x1, y0],
// 여섯 값은 [a, b, c, d, e, f] 형태의 2차원 아핀 변환입니다.
// x' = a*x + c*y + e, y' = b*x + d*y + f

작은 실습: 너비 200, 높이 300인 페이지를 90도 회전하면 표시 크기가 300×200이 되는지 계산하세요. Page::display_size()는 회전뿐 아니라 /UserUnit도 반영합니다. 크기 계산과 좌표 변환을 분리해 확인하는 습관이 이미지·지도 앱에서도 유용합니다.

⑤ UI 설계: 문서 상태와 보기 상태 나누기

crates/ui-egui/src/lib.rs의 impl eframe::App for PdfCraftApp와 ui()를 읽어 보세요. 현재 상태를 바탕으로 화면을 구성하고, 문서 변경은 crates/ui-egui/src/editing.rs를 거쳐 엔진에 전달합니다. 즉시 모드 UI라고 해서 매 프레임 PDF를 처음부터 다시 파싱해야 하는 것은 아닙니다.

확대율·현재 탭·선택 영역은 보기 상태이고, 주석·양식값·페이지 순서는 저장할 문서 상태입니다. 둘을 섞으면 확대만 했는데 “저장하지 않은 변경”이 생기는 식의 버그가 나옵니다. 어떤 동작이 undo에 들어가는지도 이 경계와 연결됩니다.

작은 실습: “확대”, “페이지 회전 편집”, “검색 다음 결과”, “주석 추가”를 보기 상태와 문서 상태로 분류하고, 각 동작에서 파일 내용이 바뀌어야 하는지 써 보세요.

⑥ 자동화 API: 사람이 보는 번호와 코드의 인덱스

도구의 페이지는 1부터, 엔진에 전달하는 배열 인덱스는 0부터 시작합니다. Automation::call()의 회전 분기가 사용하는 self.pages()와 낮은 계층의 인덱스 검증을 연결해 보세요. 외부 인자를 바로 usize로 바꾸지 않는 이유도 이해할 수 있습니다.

검색 결과도 단순한 배열만이 아닙니다. text_find의 complete·next·warnings는 “다 읽었는가”, “이어 읽을 곳이 있는가”, “실패한 페이지가 있는가”를 구분합니다. 오류가 났는데 빈 결과로 숨기지 않는 API를 배울 수 있습니다.

작은 실습: 검색이 끝났지만 한 페이지 추출에 실패한 경우를 생각하세요. complete: false이며 next는 없을 수 있습니다. “완료가 아니면 무조건 next로 재요청”하는 클라이언트가 왜 틀리는지 설명해 봅니다.

⑦ 테스트: 결과보다 불변식을 먼저 정하기

invalid_save_bytes_never_reach_publication_or_mark_saved()는 저장할 바이트가 잘못되었을 때 쓰기 콜백이 호출되지 않았는지, 기존 파일과 문서 요약이 그대로인지 함께 확인합니다. 오류 문자열 하나보다 훨씬 구체적인 동작 계약입니다.

좋은 테스트는 “성공했다”만 확인하지 않고, 실패했을 때 무엇이 그대로 남아야 하는지를 확인합니다. 이 저장소에서 배운 원칙을 메모장·이미지 편집기·설정 파일 저장에도 적용할 수 있습니다.

실습을 마친 뒤의 질문

“어떤 타입을 썼나?”에 이어 “어떤 실패를 막기 위해 썼나?”를 말해 보세요. Arc는 큰 데이터 공유, 작업 사본은 편집 실패 격리, 결과 큐 한도는 누적 방지, 저장 전 검사는 잘못된 출력의 파일 교체 방지를 담당합니다.

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

완성 앱 실행, 소스 빌드, OCR, 웹 배포의 조건을 나누어 봅니다

모든 사용 방식에 적용되는 공식 최소 CPU·RAM·VRAM 수치는 README와 주요 빌드 설정에서 확인되지 않습니다. 대신 플랫폼 조건과 메모리를 제한하는 코드값은 확인할 수 있습니다. 아래에서 운영체제 정보는 README·패키징 설정, 메모리 수치는 소스 상수 또는 계산값입니다.

사용 방식확인 가능한 조건주의할 점
데스크톱 실행 macOS·Windows·Linux·FreeBSD 배포 안내 운영체제별 그래픽 드라이버와 패키지 조건을 확인해야 합니다. 별도 서버나 데이터베이스는 필수 구성 요소가 아닙니다.
macOS 패키징 목표 11.0, universal 배포 안내 packaging/macos/Info.plist.in과 packaging/macos/package.sh의 값입니다. 모든 하드웨어에서의 실측 호환성 보장은 아닙니다.
Linux 일반 빌드 glibc 2.35 이상, RISC-V 빌드 2.39 이상이라는 README 안내 x86_64·aarch64용 여러 패키지와 RISC-V tarball이 안내됩니다. 실제 배포 파일별 조건을 따릅니다.
소스 빌드 Rust 1.95 이상, Cargo, 대상별 시스템 라이브러리 Linux CI는 X11·Wayland·xkbcommon·OpenGL·GTK 개발 패키지를 설치합니다. CI의 검증 도구까지 런타임 필수 항목인 것은 아닙니다.
웹 빌드 wasm32-unknown-unknown, Trunk 웹 진입점은 Glow를 선택합니다. 브라우저의 WebGL 지원·메모리·다운로드 권한 등도 영향을 줍니다.
OCR text-detection.rten, text-recognition.rten 릴리스에는 모델을 포함한다는 안내가 있습니다. 소스 빌드의 모델 탐색은 Models::find()를 확인하세요.

메모리는 파일 크기 하나로 결정되지 않습니다

2000×3000 픽셀 RGBA 버퍼 하나는 2000 × 3000 × 4 = 24,000,000바이트, 약 22.9 MiB입니다. 원본 PDF가 작아도 압축 이미지가 풀리고, 페이지 픽셀·글꼴 캐시·undo·작업 결과가 함께 살아 있으면 메모리는 훨씬 커집니다.

코드 위치제한 / 설정해석
crates/render/src/raster.rsMAX_SIDE = 8192.0
MAX_PIXELS = 64.0e6
래스터 크기를 제어합니다. 6,400만 픽셀 RGBA만 약 256 MB이며 다른 데이터는 별도입니다.
crates/render/src/raster/work_queue.rs완료 결과 32 MiB·32개 기준진행 중 작업·캐시 전체를 포함하지 않으며 큰 결과 하나의 예외가 있습니다.
crates/automation/src/text_cache.rs추정 텍스트 저장량 64 MiB·4,096페이지같은 자동화 세션의 모든 열린 문서가 공유하는 보관 예산입니다. 현재 호출의 임시 결과는 별도입니다.
crates/engine/src/lib.rsrender_threads()데스크톱 계산식은 사용 가능 병렬도를 2~8로 제한한 뒤 1을 빼므로 1~7개 작업 스레드를 선택합니다.

개별 버퍼와 캐시의 상한을 더해도 프로세스 전체의 최대 메모리를 보장하는 숫자는 나오지 않습니다. 실제 사용량은 문서 구조, 페이지 크기, 동시에 열린 문서와 작업 수에 따라 측정해야 합니다.

개발 빌드는 디버그 정보와 중간 산출물 때문에 디스크도 많이 씁니다. 루트의 [profile.low-disk]는 debug = "line-tables-only", incremental = false를 설정합니다. 용량을 줄이는 대신 재빌드 비용이 달라질 수 있다는 절충을 읽을 수 있습니다.

GPU와 웹 문서에서 확인할 차이

데스크톱의 apps/pdfcraft/src/main.rs는 기본 wgpu 시작이 실패하면 OpenGL을 시도하는 경로를 둡니다. PDFCRAFT_RENDERER=gl로 선택하는 코드도 있습니다. 따라서 GPU 표시 경로와 CPU 페이지 래스터화, 호환성 대체 경로를 함께 보아야 합니다.

packaging/web/README.md는 WebGPU → WebGL2 전환과 ?webgl·?cpu를 안내합니다. 그러나 커밋 0dc77fe의 apps/pdfcraft-web/src/main.rs는 eframe::Renderer::Glow를 지정하고, Cargo 기능도 glow를 켭니다. 이 진입점에는 해당 플래그의 렌더러 선택 분기가 확인되지 않습니다. 이 버전을 이해할 때는 소스에 보이는 경로를 기준으로 삼으세요.

웹 배포는 정적 파일을 제공하는 형태입니다. packaging/web/_headers는 WASM MIME을 application/wasm으로, 해시 파일 캐시를 1년·immutable로, index.html을 no-cache로 설정합니다. 배포 문서의 WASM 약 13 MB·압축 약 5 MB는 안내 수치이며 브라우저 실행 메모리와 다릅니다.

한국어 사용에서 분리할 세 가지

한국어 UI 카탈로그는 crates/ui-egui/src/i18n/ko.tsv에 있고 언어 목록에도 등록되어 있습니다. 하지만 UI 번역, PDF 안의 한국어 글꼴 처리, 스캔한 한국어의 OCR은 별개입니다. crates/ocr/src/lib.rs의 LANGUAGES는 현재 en만 선언하므로 한국어 OCR 지원으로 해석하면 안 됩니다.

CRAFT_FONTS_DIR는 일본어 글꼴을 넣는 선택적 빌드 입력으로 안내됩니다. 이 변수가 모든 CJK 편집과 한국어 문자 인식 문제를 해결하는 것은 아닙니다.

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

읽기 → 작은 자동화 → 실패 검증 순서로 난도를 올립니다

각 과제는 작은 합성 문서나 연습용 복사본을 기준으로 합니다. 명령 예시는 해당 버전의 CLI 또는 개발 환경을 준비한 독자가 수행할 실습 절차입니다. 완료 기준을 먼저 읽고, 그 기준을 입증할 결과를 남기세요.

과제 1 · 코드 탐색

회전 버튼의 여행 지도 그리기 입문

목표: 소스 수정 없이 한 기능의 입력부터 저장까지 연결합니다.

진행: page_rotate, Edit::RotatePages, Session::apply(), rotate_pages(), write_incremental()을 검색하고 호출 관계를 표로 적습니다. 각 단계에 페이지 번호의 시작값과 각도 단위를 표시하세요.

완료 기준: “사용자의 1페이지는 어느 지점에서 인덱스 0이 되는가?”, “45도는 어디서 거부되는가?”, “회전 성공 직후 디스크 파일이 바뀌는가?”에 근거 파일과 함께 답할 수 있습니다.

과제 2 · CLI와 JSON

빈 페이지를 만들고 증분 저장 비교하기 입문 → 중급

목표: 여러 명령을 하나의 세션에서 실행하고, 바이트 보존과 화면 변화를 나누어 확인합니다.

준비: 출력용 practice 디렉터리와 아래 rotate.json을 준비합니다. 새 CLI 실행의 첫 문서 ID가 1이라는 조건을 사용합니다.

[
  {"tool":"doc_create","args":{"from":"blank","width":200,"height":300}},
  {"tool":"doc_save","args":{"doc":1,"path":"before.pdf"}},
  {"tool":"page_rotate","args":{"doc":1,"pages":[1],"degrees":90}},
  {"tool":"page_render","args":{"doc":1,"page":1,"dpi":96},"out":"after.png"},
  {"tool":"doc_save","args":{"doc":1,"path":"after.pdf","full":false}},
  {"tool":"edit_undo","args":{"doc":1}},
  {"tool":"doc_info","args":{"doc":1}}
]
# 모든 출력 경로는 --root 안쪽으로 제한합니다.
pdfcraft-cli run --script rotate.json --root ./practice --report

# 원본 바이트가 시작 부분에 보존되는지만 비교합니다.
python3 - <<'PY'
from pathlib import Path
before = Path('practice/before.pdf').read_bytes()
after = Path('practice/after.pdf').read_bytes()
print(after.startswith(before), len(before), len(after))
PY

완료 기준: 초기 200×300 포인트 페이지의 표시 방향이 바뀌고, after.pdf의 시작 부분에 before.pdf가 그대로 들어 있음을 확인합니다. 이는 합성 문서에서 증분 저장을 명시한 조건의 검사입니다. 전체 재작성이나 시각적 동일성까지 판정하지 않습니다.

더 생각하기: 새 경로 저장은 기본적으로 전체 재작성인데, 여기서는 왜 full: false를 넣었을까요? 마지막 undo는 메모리 상태에만 반영되므로 저장된 after.pdf가 저절로 원래 방향이 되는지도 구분해 보세요.

과제 3 · 자료구조와 테스트

회전 연산의 계약을 테스트로 표현하기 중급

목표: 입력 검증·상태 불변·의미적 동일성을 따로 확인합니다.

진행: 개발용 사본에서 crates/organize의 테스트 패턴을 따라 정상 페이지를 만듭니다. 45도 거부, -90도 정규화, 90도 네 번의 유효 회전 복원을 검사합니다. 엔진 수준에서는 실패 전후 undo·dirty 상태도 비교하세요.

완료 기준: 실패한 연산이 기존 값을 바꾸지 않고, 네 번의 정상 회전 뒤 표시 방향이 돌아옵니다. 원래 없던 /Rotate가 명시적인 0으로 생길 수 있으므로 바이트 일치와 의미적 일치를 혼동하지 않습니다.

검증 범위: 새 테스트가 해당 계약을 실제로 관찰하게 만들고, 필요한 크레이트만 검사합니다. UI 전체나 거대한 문서 코퍼스를 먼저 돌릴 필요는 없습니다.

# 개발용 사본에서 organize 크레이트의 테스트 실행 예시
cargo test -p pdfcraft-organize -j 2
과제 4 · 동시성

오래된 렌더 결과가 새 화면을 덮지 않게 설명하기 중급

목표: 완료 순서와 사용자가 원하는 우선순위를 구별합니다.

진행: WorkQueue::replace()와 State::take()를 읽고, A페이지 요청 뒤 B페이지로 이동했지만 A가 늦게 완료되는 상황을 표로 그립니다. 배율·타일·태그가 다른 경우도 추가하세요.

완료 기준: 어떤 결과를 버리고 어떤 요청을 재시도하는지 설명할 수 있습니다. 개발용 사본에서 테스트를 확장한다면 실제 시간 지연에 의존하기보다 요청·완료 순서를 직접 구성해 재현성을 확보합니다.

과제 5 · 저장 신뢰성

실패하는 저장 장치를 흉내 내기 고급

목표: 문서 생성 실패, 동기화 실패, 파일 교체 실패를 다른 문제로 다룹니다.

진행: invalid_save_bytes_never_reach_publication_or_mark_saved()와 platform::staging::write_atomically_with()의 테스트를 읽습니다. 개발용 사본에서 주입 가능한 쓰기·동기화 함수를 이용해 실패를 돌려주는 경우를 만드세요.

완료 기준: 기존 대상 파일 바이트가 보존되고, 임시 파일이 정리되며, 저장하지 않은 변경 상태가 유지되는지 확인합니다. 잘못된 PDF 바이트는 실제 파일 쓰기 전에 거부되어야 합니다.

확장 질문: 파일 동기화 성공과 정전 뒤 디렉터리 변경의 영속성은 왜 다른가요? 서명된 PDF의 전체 재작성을 엔진이 제한하는 이유도 연결해 보세요.

과제의 완성은 기능을 한 번 실행하는 데서 끝나지 않고, 성공과 실패의 경계를 설명할 수 있을 때 드러납니다.

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

6주 동안 하나의 편집 흐름에 지식을 덧붙이는 순서입니다

이미 모든 기술을 알아야 시작할 수 있는 저장소는 아닙니다. 다만 30만 줄 이상의 Rust를 처음부터 읽는 방식은 피하는 편이 좋습니다. 아래 경로는 회전과 저장이라는 작은 기능을 계속 다시 보면서 필요한 배경을 늘리는 학습안입니다.

주차핵심 학습읽을 곳과 실습완료 기준
1주차Rust 구조체·enum·참조·Result ObjRef, OpenError, Edit::RotatePages를 읽고 과제 1 수행 입력과 반환 타입을 자기 말로 설명합니다.
2주차소유권·Arc·작업 사본 cos::Document와 Session::apply()를 읽고 상태 연결 그림 작성 공유되는 값과 복사되는 값을 구분합니다.
3주차PDF 객체·페이지 트리·좌표 model::pages(), view_matrix_for(), rotate_pages()와 과제 3 상속값과 회전 후 표시 크기를 계산합니다.
4주차직렬화·증분 기록·파일 교체 crates/cos/src/writer.rs, publish_save(), crates/platform/src/staging.rs와 과제 2 증분·전체·원자적 저장을 각각 설명합니다.
5주차작업 큐·메모리 예산·UI 상태 crates/render/src/raster/work_queue.rs, crates/automation/src/text_cache.rs, UI의 ui()와 과제 4 오래된 결과와 캐시를 언제 버리는지 설명합니다.
6주차JSON Schema·MCP·회귀 테스트 crates/automation/src/tools.rs, crates/automation/src/mcp.rs, 저장 실패 테스트와 과제 5 입력 계약·권한 범위·실패 시 보존 조건을 검증합니다.

웹에 관심이 있다면 5주차 뒤 apps/pdfcraft-web/src/main.rs로 이동해 WASM과 브라우저 이벤트를 읽으세요. 시스템 프로그래밍에 관심이 있다면 cos의 파서 예산, filters의 압축 해제 한도, crypt의 암호화 경로로 확장할 수 있습니다.

테스트에 관심이 있다면 crates/testkit의 합성 문서와 crates/oracle의 외부 검증 도구 연결을 비교하세요. 자기 코드로 만든 파일을 자기 코드로만 읽으면 같은 오해를 공유할 수 있으므로, 다른 구현으로 확인하는 발상이 중요합니다.

학습 범위를 넓힐 때도 “이 기능의 입력·상태·실패·출력은 무엇인가”라는 같은 질문을 유지하세요. 기술 이름은 달라도 큰 프로그램을 읽는 방법은 반복됩니다.

10핵심 키워드 사전

소스를 다시 읽을 때 옆에 두는 작은 번역표
용어쉬운 설명
크레이트Rust 코드의 컴파일 단위입니다. Cargo 패키지는 라이브러리·실행 파일 크레이트를 포함할 수 있습니다.
워크스페이스관련 패키지를 묶어 의존성·버전·검사를 함께 관리하는 공간입니다.
기능 플래그라이브러리의 선택 기능을 켜는 Cargo 설정입니다. 서버 코드 포함과 서버 실행은 별개입니다.
파사드복잡한 내부 기능을 한 출입구로 묶는 설계입니다. 여기서는 engine::Session이 중심입니다.
명령 레지스트리작업 ID·표시 이름·단축키·실행 조건을 모아 둔 목록입니다.
COS / 객체 그래프PDF의 기본 값과 객체 참조를 다루는 층입니다. 페이지·글꼴·이미지가 참조로 연결됩니다.
간접 참조객체 값 대신 번호와 세대로 다른 객체를 가리킵니다. ObjRef가 이를 표현합니다.
페이지 트리문서의 페이지를 계층적으로 묶은 구조입니다. 일부 속성은 부모에게서 물려받습니다.
콘텐츠 스트림페이지에 글자·경로·이미지를 그리는 PDF 명령이 담긴 데이터입니다.
지연 파싱필요한 객체를 사용할 때 읽는 방식입니다. 모든 객체를 처음부터 풀어 둘 필요를 줄입니다.
overlay원본을 직접 바꾸는 대신 변경된 객체만 따로 모아 두는 층입니다.
복사 시 쓰기변경하지 않은 데이터는 공유하고, 수정되는 부분을 별도로 갖는 방식입니다.
Arc / Weak공유 소유권 포인터와 약한 참조입니다. Weak는 그 자체로 원본의 수명을 늘리지 않습니다.
스냅샷특정 시점의 상태를 보관한 것입니다. 이 엔진의 undo는 이전 문서 상태를 저장합니다.
dirty현재 상태와 저장된 상태 사이에 아직 저장할 차이가 있다는 표시입니다.
증분 저장기존 바이트 뒤에 수정 객체와 새 참조 정보를 붙이는 저장입니다.
전체 재작성현재 필요한 객체를 새 파일 구조로 다시 쓰는 저장입니다. 이전 revision을 남기지 않아야 할 때도 필요합니다.
xref / Prev객체 위치를 찾는 교차 참조 정보와 이전 참조 구역을 가리키는 연결입니다.
원자적 파일 교체완성된 임시 파일을 대상 경로로 바꾸어, 반만 쓴 파일이 기존 파일을 대신하지 않게 하는 방식입니다.
래스터화 / 타일그리기 명령을 픽셀로 바꾸는 일과, 큰 페이지를 나눠 처리하는 작은 영역입니다.
배압소비가 밀릴 때 새 작업 생산을 늦추어 결과가 끝없이 쌓이지 않게 하는 제어입니다.
LRU 캐시오랫동안 사용하지 않은 항목부터 버리는 보관 전략입니다. 텍스트 캐시에 쓰입니다.
아핀 변환이동·회전·배율 등을 행렬 형태로 연결하는 좌표 변환입니다.
WASMRust 등의 코드를 브라우저에서 실행할 수 있는 WebAssembly 형식입니다.
JSON Schema인자 이름·자료형·필수값·범위를 기술하는 입력 계약입니다.
MCP / JSON-RPC도구·자원을 주고받는 프로토콜과 요청·응답 메시지 형식입니다. 이 구현의 전송은 표준입출력입니다.
OCR이미지 속 글자를 인식해 텍스트로 바꾸는 기술입니다. 기존 PDF 텍스트 추출과는 다릅니다.
샌드박스실행 가능한 기능과 자원을 제한한 환경입니다. 양식 스크립트에는 파일·네트워크 접근을 제공하지 않습니다.
오라클 / 퍼징다른 구현으로 결과를 비교하는 검증 기준과, 다양한 변형 입력으로 실패를 찾는 시험 기법입니다.
회귀 테스트한 번 고친 실패가 다시 생기지 않도록 그 상황을 재현해 검사하는 테스트입니다.

11참고 링크

프로젝트 현황은 날짜로, 구현 근거는 커밋으로 고정해서 읽습니다

공식 저장소·사용 문서

구현 근거를 바로 열기

관련 기술의 공식 학습 자료

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

항목상태와 근거
선정 맥락TrendShift Monthly #14 · 2026-10-23 분석
스타 / 포크17,301 / 6,644 · 2026-10-23 GitHub 기준
생성일2026-09-30 · 저장소 생성 시각이며 개발 시작일과 같다는 뜻은 아닙니다.
최신 릴리스v0.13.0 · 2026-10-23
최근 push2026-10-23 · GitHub 기준
분석 기준 커밋
소스 스냅샷
0dc77fe · 2026-10-22
Merge pull request #4129 from aiongg/fix/coderabbit-c5-forms-misc
주 언어 / 구조Rust · crates 33개 + 앱 3개 + xtask · Cargo 패키지 총 37개
코드 규모Rust 305,508줄(vendor 제외), 479,297줄(vendor 포함). 테스트·생성 데이터·주석·빈 줄 포함.
주요 설정워크스페이스 0.13.0 · Edition 2024 · 선언 Rust 기준 1.95
라이선스2026-10-23 GitHub 기준 Apache-2.0. Cargo·README·루트 라이선스 파일은 MIT OR Apache-2.0을 명시합니다.
한국어UI 카탈로그 crates/ui-egui/src/i18n/ko.tsv 있음. OCR 언어 선언은 en만 있음.
개발 단계README 기준 알파. 지표와 버전 번호가 업무 호환성·보안 완성을 뜻하지는 않습니다.

릴리스 날짜와 소스 커밋 날짜는 다른 기준입니다. 위 구현 설명은 0dc77fe에 고정되어 있으므로, 이후 push나 릴리스 바이너리의 변경을 자동으로 포함하지 않습니다. 파일 줄 수도 이 스냅샷의 값입니다.

쓰기 전에 알아둘 한계
학습 가치와 실제 문서 업무의 적합성을 함께 판단하세요

① 알파이며 호환성 범위가 제한됩니다. README는 기존 텍스트 편집, 특히 CJK·오른쪽에서 왼쪽으로 쓰는 문자, 오피스 변환, 전문 인쇄·서명·XFA 등의 부족한 부분을 명시합니다. 중요한 문서는 복사본으로 열고 저장 후 다른 도구에서도 결과를 확인하는 흐름이 적합합니다.

② 저장 방식에는 예외가 있습니다. 암호화 변경·손상 복구·민감 내용 삭제는 전체 재작성이 필요할 수 있습니다. 서명된 문서는 전체 재작성 제한도 있습니다. “원본 바이트는 언제나 보존된다”는 문장만으로 모든 저장 동작을 설명할 수 없습니다.

③ 로컬 처리와 네트워크 기능은 구분해야 합니다. README는 계정·텔레메트리·클라우드 처리 없이 동작한다고 설명합니다. 한편 apps/pdfcraft/src/updates.rs의 latest_release()는 업데이트 확인 때 GitHub API에 요청하고, 웹의 ?file= 경로는 지정한 URL에서 문서를 받습니다. 로컬 편집 구조를 네트워크 코드 부재로 확대 해석하지 마세요.

④ 테스트 보고는 전체 안전성 보장이 아닙니다. README도 적대적 파일 퍼징에서 충돌·멈춤이 계속 발견된다고 밝힙니다. 코퍼스의 충돌 0건과 모든 입력에서 충돌이 없다는 주장은 다릅니다. 메모리·속도·프린터 품질 역시 실제 환경별 확인이 필요합니다.

⑤ 코드와 자산의 조건이 모두 같지는 않습니다. 코드의 이중 라이선스와 별개로 글꼴·아이콘·자료의 출처는 ATTRIBUTION.toml에 기록됩니다. ArtCraft 이름·로고에는 별도 상표 조건이 있습니다. 코드 라이선스 한 줄을 모든 자산의 조건으로 읽으면 안 됩니다.