정의
Notion처럼 블록으로 이루어진 문서 편집기를 만들 때, 문서 데이터의 주인은 내 앱이 쥐고 있으면서 무거운 리치텍스트 편집 엔진은 "지금 커서가 놓인 블록 하나에만" 잠깐 붙였다 떼는 구조를 쓰고 싶을 때의 아키텍처다. 문서 전체를 통째로 하나의 편집기에 맡기지 않는 것이 핵심이다.
Focused guest editor(역주: 이런 표준 용어는 없다, 이 글에서 붙인 가칭임)는 문서의 의미, 즉 블록 트리(블록들의 계층 구조)·패치(변경 내역)·권한을 host(문서 데이터의 실제 주인 = 내 앱 코드)가 소유하고, 리치텍스트 엔진은 현재 포커스된 텍스트 블록에만 잠시 마운트되는 guest로 두는 방식이다. guest는 inject(host가 그 블록의 현재 값을 넣어줌) / commit(편집한 내용을 host에 돌려줌) / keymap(키보드 단축키 매핑) 위임 같은 좁은 채널로만 host와 대화한다. 문서 전체를 하나의 에디터 문서(doc)로 두는 흔한 모델과는 정반대다.
왜 필요한가
문서 전체를 단일 리치텍스트 문서로 두면, 블록 트리·변경 히스토리·권한·실시간 구독이 전부 에디터의 projection(에디터가 내부적으로 만드는 문서 표현)에 묶여 종속된다. 그러면 블록마다 다른 권한, 블록 바깥의 위젯, 여러 블록에 걸친 제스처를 붙이기 어렵다. host가 source of truth(진실의 원천 = 이 데이터의 유일한 기준)이고 guest는 포커스 lease(잠깐 빌리는 편집 권한)만 가지면, 인라인 편집 사용감은 그대로 두면서 문서의 의미는 내 도메인 모델에 남길 수 있다.
동작 원리
- Host가 블록 배열과 패치 apply 관문을 가진다. 비포커스 블록은 정적 렌더(또는 읽기 전용)다.
- 사용자가 텍스트 블록을 포커스하면 guest가 그 블록에만 마운트되고, host가 현재
content를 inject한다. - 타이핑·마크는 guest 세션 안에서 처리된다. 구조 연산(타입 변경, 분할, 이동)은 host 콜백으로 위임한다.
- 포커스 상실·퇴거 시 guest가 commit으로 내용을 host에 반납하고 언마운트한다.
- split/merge 직후 늦은 blur commit이 새 상태를 덮어쓰지 않도록, 구조 연산 전 commit-before-delegate와 lease retire가 필요하다.
| 항목 | 설명 | 주의점 |
|---|---|---|
| Host SoT | 트리·패치·권한·autosave | guest 내부 doc ≠ 문서 전체 |
| Focus lease | 포커스 블록에만 guest | 언마운트 시 commit 필수 |
| 통신 채널 | inject / commit / keymap 위임 | 채널 밖 돌연변이 금지 |
| Stale commit | 늦은 blur가 이후 패치를 덮음 | retire·commit-before-delegate |
| 키 라우팅 3층 | 텍스트=guest, 구조=host, 무포커스=selection | 모드를 섞으면 키 충돌 |
실무 적용
크로스블록 텍스트 이동은 guest가 한 블록만 소유하므로 guest 네이티브 DnD로 풀 수 없다. 컨테이너에서 HTML5 DnD를 듣고 host 패치로 반영한다. 포커스 스토어를 React 밖에 두면 useSyncExternalStore로 구독하며, 스냅샷 동일성 규칙을 지킨다.
트레이드오프
- 블록 수와 무관하게 guest 인스턴스가 하나(또는 소수)라 비용이 예측 가능하다. 대신 포커스 전환마다 mount/commit 비용과 race 처리가 생긴다.
- host/guest 경계를 지키면 테스트·권한이 명확해지지만, “문서 전체가 하나의 에디터” API에 익숙한 라이브러리 기능을 그대로 쓰기 어렵다.
사용하면 안 되는 경우
- 문서가 짧은 단일 필드(댓글 한 줄)일 때 — guest/host 분리가 과하다.
- 협업 CRDT가 문서 전체를 하나의 공유 타입으로 다루는 모델이 이미 SoT일 때 — 경계를 억지로 나누면 동기화 비용만 는다.
흔한 실수
- 비포커스 블록에도 guest를 마운트해 “편의”를 취하다 성능·포커스 race가 난다.
- blur commit과 구조 패치 순서를 보장하지 않아 stale overwrite가 난다.
- 텍스트 키와 구조 키를 한 keymap에 섞어 Mod-D 같은 단축키가 모드마다 다르게 깨진다.
- 크로스블록 이동을 guest DnD로 구현하려다 실패한다.
관련 개념
- two-stack-inverse-undo — host 문서 히스토리와 guest 세션 히스토리를 층으로 나눈다.
- usesyncexternalstore-snapshot-identity — 포커스/레이아웃 스토어를 외부 상태로 둘 때 스냅샷 동일성.
- html5-drag-and-drop — guest 경계를 넘는 텍스트 이동 채널.