.docx를 열어 AI가 문단 하나를 고쳐도, 건드리지 않은 나머지는 원본 바이트 그대로 유지하고 바뀐 문단만 다시 만들어 끼워 넣는다. 그래서 워드로 다시 열어도 서식이 깨지지 않는다 — 다른 변환 도구들이 문서를 통째로 다시 그리다 서식을 망가뜨리는 것과 정반대다.
genspark-ai/genoffice · 제작 Mainfunc, Inc.(Genspark) · 라이선스 Apache-2.0(단 ee/는 엔터프라이즈) · 언어 TypeScript 770파일 + Rust 7파일 · ~30.8만 줄 · 테스트 303개 · 구조 Electron 앱 5개 + 엔진 패키지 11개 모노레포 · 배포 버전 v0.4.110 · 최신 커밋 2026-08-03(내부 저장소를 주기적으로 미러링하는 "Sync snapshot") · TrendShift Daily #10)
genoffice는 "워드·엑셀·PPT·PDF를 편집하는 5개의 Electron 데스크톱 앱을, 하나의 공용 AI 엔진 위에 얹고, 원본 파일을 최대한 안 건드리는 패치 방식으로 저장하도록 만든, 오픈소스 AI 네이티브 오피스 스위트"다. 겉보기엔 무료 MS 오피스 같지만, 속을 보면 "파일 포맷 엔진 + AI 에이전트 루프"를 직접 구현한 대형 모노레포다.
보통의 문서 변환 도구로 .docx를 고치는 건 책 전체를 다시 타이핑하는 것과 같다. 겉보기엔 똑같아도 글꼴·자간·숨은 서식이 조금씩 어긋난다. 워드로 다시 열었을 때 "어, 왜 줄이 밀렸지?" 하는 그 현상이다.
genoffice는 옛날 활판 인쇄공처럼 일한다. 원본 판(원본 파일의 바이트)은 그대로 두고, 당신이 고친 그 한 문단의 활자만 새로 짜서 그 자리에 끼운다. 나머지 줄의 잉크는 한 점도 안 건드린다. 그래서 다시 열면 당신이 고친 부분 말고는 원본과 완전히 동일하다.
apps/docs·apps/sheets·apps/slides·apps/pdf 앱으로 만들고, apps/shell이 이들을 탭으로 품는 "런처"가 된다.에이전트가 문서 하나를 여는 순간부터 AI가 문단을 고치고 저장할 때까지, genoffice 안에서 벌어지는 일을 압축하면 이렇다:
여기서 결정적인 건 ①과 ⑤·⑥이다. 원본을 통째로 다시 그리는 게 아니라, 고친 문단만 골라 갈아 끼우고 나머지는 원본 바이트를 복사한다. 이 "narrow patch(좁은 패치)" 철학이 docs뿐 아니라 sheets·slides에도 똑같이 흐른다 — 원본 파일이 언제나 진실의 근원이고, 편집은 최소한의 조각 교체로 적용된다.
첫째, AI가 껍데기가 아니라 뼈대다. 대부분의 오피스에 붙은 "AI 도우미"는 옆 창에서 텍스트를 뱉는 챗봇이고, 문서에 반영하려면 사람이 복붙해야 한다. genoffice는 반대다 — AI가 문서 상태를 읽고(read_blocks), 블록을 갈아 끼우고(replace_blocks), 차트를 그리는(insert_chart) 툴을 직접 호출한다. 워크북·슬라이드·PDF 상태 위에서 도는 도구 호출 에이전트(tool-calling agent)가 각 앱에 내장돼 있다.
말로 지시하는 비서 vs 손이 달린 비서. 보통의 오피스 AI는 "이렇게 고치면 어때요?" 하고 말만 하는 비서다 — 실제 수정은 당신 몫이다. genoffice의 AI는 손이 달려서 직접 문서의 그 문단을 지우고 새로 쓰고, 표에 수식을 넣고, 슬라이드에 차트를 꽂는다. 대신 무슨 짓을 했는지 스냅샷으로 남겨 한 번에 되돌릴 수 있게 해둔다.
둘째, 원본 보존이 곧 호환성이다. 실무에서 오픈소스 오피스가 외면받는 가장 큰 이유는 "워드로 만든 파일을 열었다 저장하면 서식이 틀어진다"는 것이다. genoffice의 바이트 보존 왕복은 이 지점을 정면으로 노린다. 손 안 댄 문단은 원본 XML 조각 그대로, 스타일·이미지·주석·수식은 zip 엔트리 그대로 복사하니, 회사에서 돌려보는 워드 파일을 깨뜨리지 않고 AI로만 고칠 수 있다. 테스트가 이걸 못 박는다 — "no edits → byte-identical output including comments.xml"(편집이 없으면 주석까지 포함해 바이트 동일).
남의 변환 라이브러리에 기대지 않고 docx·pptx 엔진을 직접 짰기에 얻는 것들 — 정밀 제어(문단 단위 패치, 추적 변경·잉크·수식 보존), 고충실도 렌더링(원본 레이아웃의 줄 metrics 재현), AI와의 결합(블록 트리가 곧 AI가 다루는 단위). 포맷을 손수 파싱했기에 "AI가 문서의 구조를 이해하고 좁게 편집"하는 게 가능해진다.
셋째, 다섯 앱이 한 엔진을 공유한다는 공학적 절제다. AI 에이전트 루프(agent-core), 모델 추상화(ai-provider), 검색(ai-search)을 패키지로 빼서 docs·sheets·slides·pdf가 똑같이 끼워 쓴다. 앱마다 AI를 새로 만들지 않고, 스킬(skill)이라는 플러그인으로 "이 앱에서 AI가 쓸 수 있는 툴 묶음"만 갈아 끼운다. 오피스 4종을 한 팀이 유지보수할 수 있는 구조적 비결이다.
기본 설정에서 AI는 Genspark 계정으로 로그인해 모델 호출을 Genspark 서버로 라우팅한다(로컬에 API 키를 저장하지 않음). 오픈소스이고 Anthropic·Gemini·OpenAI 키를 직접 넣는 길도 열려 있지만, 편하게 쓰려면 회사 서비스에 묶인다는 뜻이다. 또 이 저장소는 내부 개발 저장소를 주기적으로 미러링한 "Sync snapshot"이라, 이슈·커밋 히스토리가 일반 OSS처럼 촘촘하지 않다.
genoffice는 레이어가 뚜렷한 스택이다. 위에서부터 ① Electron 앱(셸+4 편집기), ② 각 편집기의 렌더러 UI(React), ③ 순수 TypeScript 포맷 엔진, ④ 공용 AI 계층, 그리고 sheets만 가진 ⑤ Rust 사이드카다.
| 레이어 | 무엇 | 쓰인 기술 |
|---|---|---|
| 데스크톱 셸 | 앱 컨테이너·자동 업데이트·창 관리 | Electron, electron-utils(메인 프로세스 헬퍼) |
| Docs 편집기 | .docx 워드프로세서 UI | React, TipTap(스트리밍 편집기), 페이지네이션 뷰 |
| Sheets 편집기 | .xlsx 표계산 UI | React, Univer 코어(Apache-2.0) + 자체 확장, Konva(차트) |
| Slides 편집기 | .pptx 프레젠테이션 UI | React, 자체 pptx 렌더러, HarfBuzz(harfbuzzjs) 텍스트 셰이핑 |
| PDF 편집기 | PDF 뷰어·주석·서명 | pdf.js(렌더) + pdf-lib(편집) |
| 포맷 엔진 | 파일 파싱·생성·패치 | docx-engine·pptx-engine·pptx-render (fast-xml-parser, jszip) |
| xlsx 사이드카 | 엑셀 import/export·재계산 | Rust: calamine(읽기) + IronCalc(수식) + quick-xml |
| AI 계층 | 에이전트 루프·모델·검색 | agent-core, ai-provider(스트리밍), ai-search |
| 공용 인프라 | 다국어·UI 킷·최근파일 | i18n, ui(React 킷), project-store |
word/document.xml. genoffice의 포맷 엔진은 이 XML을 직접 파싱·생성·패치한다. fast-xml-parser로 읽고 jszip으로 zip을 풀고 다시 묶는다.harfbuzzjs는 그 표준 엔진의 JS 포팅. slides가 이걸로 PPT의 글자 위치를 원본과 똑같이 재현한다 — 폰트 metrics는 opentype.js, 양방향 텍스트는 bidi-js.다섯 앱은 각자 메인 프로세스(Node)와 렌더러(React UI)로 나뉜 Electron 앱이고, 셸이 이들을 탭으로 품는다. 앱마다 AI 패널이 있지만, 그 밑의 에이전트 루프·모델 호출은 공용 패키지를 공유한다.
이 레포의 심장이다. 발상은 "원본은 진실, 편집은 좁은 조각 교체". 파싱 단계에서 문단마다 docxIndex(원본에서 몇 번째 요소인가)와 원본 XML 조각을 앵커로 달아둔다. 저장할 때 이 앵커를 기준으로, dirty가 아닌 블록은 원본 조각을 그대로 쓰고 dirty 블록만 새 OOXML로 만든다.
# SaveBlock — 저장할 때 각 블록이 취하는 4가지 형태 (patch.ts) { kind: 'original', docxIndex } # 원본 조각 그대로 복사 (안 건드림) { kind: 'generated', block } # 편집기가 고친 블록 → 새 OOXML 생성 { kind: 'xml', xml, docxIndex } # 편집기가 만든 자립 조각(새 표 등) { kind: 'image' | 'chart', ... } # 새 이미지·차트 → media/relationship 추가
generated 블록은 "이미 있는 스타일만 참조"한다는 규칙을 지킨다 — AI가 멋대로 새 서식 정의를 만들어 원본 스타일 표를 오염시키지 않도록. 그리고 saveDocx는 바뀐 조각을 원본 document.xml에 스플라이스한 뒤, zip의 다른 모든 엔트리(스타일·이미지·주석·테마·잉크·수식)를 바이트 단위로 복사해 재압축한다. 테스트가 이 불변식을 촘촘히 지킨다 — "편집 없으면 comments.xml까지 바이트 동일", "손 안 댄 bidi 문단 바이트 동일", "cross-paragraph 범위의 마커 보존" 등.
포토샵 레이어 vs 사진 재촬영. 사진을 고칠 때 원본 위에 레이어 하나만 얹어 그 부분만 바꾸면 원본 픽셀은 그대로다. 반대로 사진을 다시 찍으면(재렌더링) 미세하게 다 달라진다. genoffice는 레이어 방식이라, "고친 곳"과 "원본 그대로"가 명확히 분리된다.
모든 앱이 공유하는 AI의 두뇌. 순수 TypeScript(Electron 의존 없음)이고, "스킬"이라는 플러그인으로 앱별 능력을 끼운다. 눈여겨볼 설계가 세 가지다.
snapshotBefore). 그래서 AI가 한 수정을 원클릭으로 통째 되돌릴 수 있다. "손 달린 비서"의 안전장치.get_document_context·read_blocks·replace_blocks·insert_chart 등 10개 툴을 스킬로 노출한다.엑셀은 수만 행·복잡한 수식이 흔해 JS만으로는 버겁다. sheets는 무거운 일을 별도 Rust 프로세스(사이드카)로 내린다. JS와는 stdin/stdout에 한 줄씩 JSON을 주고받는 라인 프로토콜로 통신하고, 세션(sessionId)으로 열린 워크북 상태를 유지한다.
# xlsx 사이드카가 받는 명령들 (main.rs, JSON 라인 프로토콜) Open # 워크북 열기 → sessionId 발급 ReadRange # 특정 시트의 셀 범위 읽기 RecalcCells # 편집한 셀 기준 수식 재계산 (IronCalc) ConvertWorkbook / SaveArchive # 포맷 변환 · 저장 ScanEntries / ReadMedia / ArchiveManifest # zip 내부 탐색·미디어
여기서도 "원본 보존" 철학이 반복된다 — 테스트 이름이 그대로 증거다: "preserves unrelated workbook parts and updates Sheet1 in place"(관련 없는 워크북 부분은 보존하고 Sheet1만 제자리 갱신). 문서든 표든, 건드린 곳만 바꾸고 나머지는 원본 그대로가 이 레포 전체를 관통하는 하나의 원칙이다.
읽는 순서를 추천하면 이렇다: README → package.json(스크립트·워크스페이스) → packages/docx-engine/src/patch.ts(바이트 보존의 핵심) → packages/agent-core/src/loop.ts·skill.ts(AI 루프) → apps/docs/src/renderer/ai/(엔진이 앱에서 어떻게 쓰이는지). 엔진 패키지는 UI가 없어 단위 테스트만으로 동작을 이해할 수 있으니, 각 *.test.ts가 훌륭한 명세서가 된다.
docx·xlsx·pptx가 "zip 안의 XML"이라는 사실과, 그걸 다루는 실전 코드를 볼 수 있다. jszip으로 풀고 fast-xml-parser로 파싱해 word/document.xml의 w:p(문단)·w:tbl(표)를 다루는 흐름은, 오피스 자동화·문서 생성 도구를 만들 때 그대로 쓰인다. 배울 것: 왜 문서 편집이 "XML 조각 스플라이스"가 되는가, relationship·media·styles가 어떻게 얽히는가.
"전체 재생성" 대신 "dirty만 재생성 + 나머지 원본 복사"라는 불변식 중심 설계. 이건 문서 편집에만 국한되지 않는다 — 코드 포매터, 마이그레이션 도구, 설정 파일 편집기 등 "사람이 만든 원본을 최소한만 건드려야 하는" 모든 곳에 적용되는 사고법이다. 배울 것: 앵커(docxIndex) 기반 재조립, "손 안 댄 건 바이트 동일"을 테스트로 못 박는 방법.
agent-core는 LangChain 같은 프레임워크 없이 에이전트 루프를 밑바닥부터 짠 좋은 교보재다. 턴 제한(기본 8), 이력 제한(40), 바이트 예산 압축, 툴 입력 파싱 실패 재시도(최대 3), 스냅샷/롤백, 스트리밍 이벤트까지 — 실전 에이전트에 필요한 안전장치와 자원 관리가 응축돼 있다. 배울 것: 왜 메시지 개수가 아니라 바이트로 컨텍스트를 재는가, 스킬 합성으로 앱별 능력을 어떻게 조립하는가.
JS 앱이 무거운 연산(엑셀 파싱·수식)을 Rust 사이드카로 내리고 JSON 라인 프로토콜로 통신하는 패턴. Electron/Node 앱에서 성능 병목을 우회하는 실전 아키텍처다. 배울 것: 프로세스 간 라인 프로토콜 설계, 세션 기반 상태 관리, JS↔Rust 경계 나누기.
slides가 opentype.js(폰트 metrics)·harfbuzzjs(셰이핑)·bidi-js(양방향)로 PPT 글자를 원본과 똑같이 배치하고, pptx-render가 EMU→px 좌표계와 데이터 기반 RenderTree로 도형·채우기·선을 그린다. 배울 것: 문서 좌표계 변환, 폰트 metrics로 줄바꿈 계산하기, Konva 같은 캔버스 라이브러리를 얇게 감싸기.
packages의 엔진은 Electron 무의존 순수 TS다. docx-engine을 import해 샘플 .docx를 파싱→블록 트리 출력→한 문단만 바꿔 저장한 뒤, 원본과 diff로 비교해 "정말 그 문단만 바뀌었는지" 눈으로 확인해 보라. npm run fixtures로 테스트 .docx를 생성할 수 있고, 각 *.test.ts가 사용 예시다.
| 구분 | 요구 |
|---|---|
| 실행(사용자) | macOS(Apple Silicon) 서명된 .dmg 또는 Windows(x64) .exe 설치. 릴리스 페이지에서 내려받아 설치. |
| AI 사용 | Genspark 계정 로그인(기본) 또는 Anthropic/Gemini/OpenAI API 키 직접 입력. |
| 개발 — 공통 | Node.js ≥20, npm ≥10. npm install → npm run dev(4 편집기 + 셸 동시 실행, Vite 개발 서버). |
| 개발 — sheets | 추가로 Rust 툴체인(cargo on PATH). npm run build -w @genoffice/sheets가 xlsx 사이드카를 자동 컴파일. |
| 테스트 | npm test(엔진+앱 단위 테스트; docs/sheets/slides는 디스플레이 불필요), npm run typecheck, npm run test:e2e(Playwright). |
| 패키징 | npm run dist:mac(dmg) / npm run dist:win(nsis). 빌드 시 서드파티 라이선스 고지 자동 재생성. |
번들 폰트(Liberation·Carlito·Caladea·Noto CJK 서브셋)는 OFL/Apache, 런타임 의존성은 전부 MIT/Apache-2.0/OFL로 정리돼 있다 — 상업적 재배포까지 염두에 둔 라이선스 위생이다. 다만 ee/ 디렉토리만은 별도의 GenOffice Enterprise License이고 외부 기여를 받지 않는다(CODEOWNERS로 강제).
이 저장소는 내부 개발 저장소를 주기적으로 통째 미러링한 것이라(최신 커밋도 "Sync snapshot (2026-08-03)"), 일반 OSS처럼 세밀한 커밋 단위 히스토리나 활발한 이슈 트래킹을 기대하긴 어렵다. 코드를 읽고 배우는 데는 최상급이지만, "기여해서 머지받기"의 협업 흐름은 제한적일 수 있다.
아무 워드 파일의 확장자를 .zip으로 바꿔 압축을 풀어 보라. word/document.xml을 열어 <w:p>(문단)·<w:r>(런)·<w:t>(텍스트)를 눈으로 찾아본다. 그런 뒤 genoffice의 docx-engine/src/parse.ts가 바로 이 요소들을 어떻게 블록으로 바꾸는지 대조한다. 목표: "문서 편집 = XML 편집"을 체감.
레포를 클론해 npm install → npm run fixtures로 테스트 .docx 생성. 작은 Node 스크립트에서 docx-engine으로 파일을 파싱하고, 문단 하나의 텍스트만 바꿔 저장한 뒤, 원본과 저장본을 unzip해 diff -r로 비교하라. 목표: "정말 바뀐 파트만 달라지고 나머지는 동일"함을 직접 확인.
agent-core의 AgentSkill 인터페이스를 구현해 아주 단순한 스킬(예: "문서의 단어 수를 세는 count_words 툴")을 만든다. composeSkills로 기존 스킬과 합쳐 루프에 넣고, 툴 호출→실행→응답이 도는 걸 로그로 관찰하라. 목표: 도구 호출 에이전트의 최소 골격을 손에 익히기.
apps/sheets/native/xlsx-engine/src/main.rs의 JSON 라인 프로토콜(Open/ReadRange/RecalcCells)을 읽고, 작은 Rust(또는 Node) 프로그램으로 stdin에서 JSON 한 줄을 읽어 처리하고 stdout으로 응답하는 사이드카를 직접 만들어 본다. 세션 상태를 HashMap<sessionId, ...>로 유지하라. 목표: 프로세스 간 라인 프로토콜과 세션 설계 이해.
docx-engine의 테스트를 읽고, 어떤 편집이 "손 안 댄 부분까지 바뀌게" 만드는지 가설을 세워 실험하라(예: 특정 필드·잉크·수식이 섞인 문단). 실패 케이스를 *.test.ts 형식의 테스트로 재현해 본다. 목표: 불변식이 어디서 깨지는지 파고들며 포맷의 엣지 케이스를 학습.
| 주차 | 주제 | 구체적으로 |
|---|---|---|
| 1주차 | OOXML 포맷 기초 | docx/xlsx/pptx의 zip+XML 구조, ECMA-376 개요, jszip·fast-xml-parser로 파싱 실습. genoffice parse.ts 읽기. |
| 2주차 | 원본 보존 패치 | patch.ts·generate.ts 정독. 앵커 기반 재조립, SaveBlock 4형태, "스타일은 참조만" 규칙. 관련 *.test.ts로 불변식 확인. |
| 3주차 | Electron 앱 구조 | 메인/렌더러/프리로드 분리, IPC, 자동 업데이트. shell이 4앱을 탭으로 호스팅하는 방식. 렌더러 보안(sandbox)·SECURITY.md. |
| 4주차 | 리치 텍스트 편집기 | ProseMirror/TipTap 모델, 문서 스키마, 스트리밍 편집·dirty 추적. docs 앱의 편집기 연동 코드. |
| 5주차 | 도구 호출 에이전트 | agent-core loop·skill·types 정독. 턴/이력/바이트 예산, 스냅샷 롤백, 스트리밍. ai-provider의 모델 추상화·스트리밍. |
| 6주차 | 네이티브 사이드카 | Rust calamine·IronCalc, JSON 라인 프로토콜, 세션 상태. JS↔Rust 경계 설계와 빌드 연동(cargo). |
| 7주차 | 렌더링·텍스트 셰이핑 | pptx-render의 EMU→px, RenderTree, opentype.js metrics·harfbuzzjs 셰이핑·bidi-js. Konva 캔버스. |
| 8주차 | 통합·패키징·라이선스 | Playwright e2e, dist:mac/win, 서드파티 고지 자동화, OFL/Apache 폰트, ee/ 라이선스 경계. |
word/document.xml. 포맷 엔진이 이 XML의 w:p·w:tbl을 직접 다룬다.README.md(개요·아키텍처 노트) · packages/docx-engine/src/patch.ts·generate.ts(바이트 보존) · packages/agent-core/src/loop.ts·skill.ts(에이전트) · apps/sheets/native/xlsx-engine/src/main.rs(사이드카)