에디터 상태를 문서로 저장하지 않기
Notion처럼 블록 단위로 편집하는 문서 모델이다. 다만 React나 ProseMirror를 안쪽에 넣지 않고, 문서 구조와 편집 규칙만 TypeScript로 만들었다. UI는 sdui-document-react, 렌더링은 sdui-template이 맡는다.
에디터의 내부 상태가 곧 저장 데이터가 되면 서버 검증이나 UI 교체가 어려워진다. 그래서 문서는 에디터보다 오래 남는 데이터라는 기준을 먼저 세웠다.
Markdown ↔ 블록 문서 → patch 적용 → SDUI 또는 React로 렌더링

같은 블록 문서를 읽기 전용 Notion 테마로 렌더링한 Storybook catalog.
배열 index 대신 block anchor
삽입, 수정, 이동, 분할, 병합을 하나의 patch 형식으로 표현한다. 배열 index는 앞선 편집 하나에도 쉽게 어긋나서, 블록 ID를 기준으로 before와 after 위치를 잡았다. 잘못된 anchor를 만났을 때 임의의 위치에 넣지 않고 오류를 내도록 했다.
patch 하나를 적용하는 코드
import {
applyDocumentPatch,
createDocumentBlock,
} from "@lodado/sdui-document";
const next = applyDocumentPatch(content, {
type: "block.insert",
parentId: "root",
after: "problem",
block: createDocumentBlock({
id: "decision",
type: "document.callout",
state: { text: "문서는 에디터보다 오래 남는다." },
}),
});
// content는 그대로 두고 next에 새 문서를 반환한다.
applyDocumentPatch는 원본을 건드리지 않고 새 문서를 반환한다. 같은 로직을 브라우저와 서버에서 함께 쓸 수 있고, autosave와 undo/redo도 이 patch를 기준으로 동작한다.
설계 철학
문서는 에디터의 내부 상태가 아니다
공개 모델에는 ProseMirror transaction, DOM selection, React state를 넣지 않았다. 편집 UI를 바꾸더라도 문서 구조와 저장 데이터는 그대로 남아야 하기 때문이다. 패키지는 문서가 무엇인지만 정의한다.
모든 편집은 patch로 표현한다
삽입·수정·삭제·이동을 직접 mutation하지 않고 patch로 남긴다. 같은 patch를 optimistic UI, autosave, undo/redo, 충돌 처리에서 다시 쓸 수 있다. 변경을 데이터로 다루면 저장 전후의 흐름도 테스트하기 쉽다.
먼저 블록 단위 문제를 푼다
문서 구조, 블록 이동, 권한, 저장 상태를 먼저 안정화했다. 글자 단위 CRDT와 복잡한 inline 편집은 필요가 확인될 때 추가한다. 처음부터 모든 rich text 문제를 품지 않는 것이 이 패키지의 범위다.
부수효과는 adapter 밖에 둔다
patch 적용, 권한 판정, autosave 상태는 pure function과 reducer로 작성했다. DB, 파일 저장소, 검색, 네트워크 타이머는 interface만 제공하고 구현하지 않는다. 덕분에 브라우저·서버·worker에서 같은 규칙을 쓸 수 있다.
클라이언트 권한은 보안이 아니다
클라이언트의 권한 판정은 버튼을 숨기거나 읽기 전용 UI를 만드는 용도다. 실제 저장 요청에서는 서버 adapter가 같은 권한을 다시 확인해야 한다.
아직 넣지 않은 것
DB와 검색 구현은 adapter 밖에 두었고, 실시간 협업용 CRDT도 아직 넣지 않았다. 실제 요구가 생기기 전까지는 블록 단위 patch를 단순하게 유지하는 편이 낫다고 봤다.
/about 이력서는 이 문서 모델과 React 편집기를 실제로 사용해 작성하고 렌더링한다.