이 글에서 다루는 것
문서 편집기 템플릿을 Notion형 깊이(문서 안에 하위 문서가 이어지는 구조)까지 확장한 하루다. 아침에는 명령형 shell 정리, 이어 page 블록·push/peek/breadcrumb, collection·embed 블록, seeded gradient 표면, 마지막으로 read-only viewer 번들 분리까지 한 줄기로 맞췄다. 각 작업이 어떤 상황에서 무엇을 목표로 어떻게 풀렸고 무엇을 배웠는지를 작업별로 정리한다.
하루 요약
| 작업 | 무엇을 하고 싶었나 | 한 일 | 결과 |
|---|---|---|---|
| Navigator | page 블록 + push/peek/back + breadcrumb + slash “Page” | documentId를 가리키는 page 블록, navigator가 push/peek/back 스택 관리, peek은 readOnly surface 재사용 | 공간적 깊이 데모 가능, peek 마크업 drift 없음 |
| Archive | 삭제 patch 체크포인트에서 page id 서브트리 수집 | applyPatches 전후 체크포인트에서 page documentId 모아 repository에 전달 | “블록 삭제”와 “자식 문서 정리”를 한 patch로 묶음 |
| Surface | cover 없이도 완성된 카드 — 제목 해시 → 파스텔 gradient | seeded gradient 표면 + 모션 축소 상태 | 허접한 그리드 → 스크린샷 회귀로 고정된 카드 |
| Viewer | @pkg/viewer + viewer.css, PM/dnd import 0건 CI | subpath + chrome layer 제외 CSS로 import graph 분리, readOnly prop으로 BlockRenderer 공유 | peek/embed 경로에서 편집기 코드 빠져 번들 가벼워짐, CI로 0건 고정 |
| Embed bridge | document 안 SDUI, SDUI 안 document — 단방향 variable bridge | 단방향 variable bridge로 embed 연결 | 순환 없이 양쪽 embed 성립 |
| Shell 정리 | hook 비대화 막기 — IME·clipboard 회귀 방지 | pure 함수 추출 후 characterization 테스트 | 관련 패키지 전 suite green 유지 |
1. 계층형 문서 네비게이션
배경지식 (개념)
- Page block: 본문 블록이 다른
documentId를 가리키는 “문서 안의 문서” 링크. - Peek: 전체 페이지 이동(route) 없이 오버레이 창(dialog)에서 자식 문서를 여는 UX.
어떤 상황이었나
flat 단일 editor만 있으면 “홈 → 프로젝트 → 세부” 공간적 깊이를 데모하기 어렵다.
핵심 작업
page 블록이 documentId를 가리키고, navigator가 push/peek/back 스택을 관리한다. peek dialog는 readOnly editor surface를 재사용해 viewer markup drift를 막는다. archive는 applyPatches 전후 체크포인트에서 page documentId를 모아 repository에 넘긴다. async fetch는 (newId, 이전 문서 content) 중간 프레임을 남길 수 있어, id 변경 시 render-time reset 또는 key={documentId}로 stale content를 차단한다. 결과적으로 공간적 깊이 데모가 가능해졌고 peek 마크업 drift가 없어졌다.
교훈
- navigator는 injectable interface(
push/peek/back)로 두면 Storybook·테스트가 쉽다. - archive는 “블록 삭제”와 “자식 문서 정리”를 한 patch 체크포인트로 묶는다.
→ Hierarchical document navigation
2. Viewer 번들 분리
배경지식 (개념)
- 번들(bundle) / import graph: 한 진입점에서
import로 딸려 오는 모듈 전체가 번들이다. 무엇이 무엇을 import하는지의 관계망이 import graph이고, 이게 커지면 번들이 무거워진다. - tree-shaking: 실제로 안 쓰는 export를 번들에서 걷어내는 최적화. import 경계를 잘못 두면 안 쓰는 코드도 딸려 온다.
- Viewer entry: 편집 UI(ProseMirror, dnd-kit 등) 없이 읽기 전용 렌더만 노출하는 패키지 진입 경로(subpath).
어떤 상황이었나
gallery 카드·peek 미리보기가 편집기 barrel을 import하면 ProseMirror·dnd-kit이 embed 경로까지 따라온다 — 읽기만 하는 화면인데 편집기 코드가 통째로 딸려 와 번들이 무거워진다.
핵심 작업
@pkg/viewer subpath + chrome layer 제외 CSS로 import graph를 분리하고, viewer module graph에 PM/dnd 0건을 lint/CI로 검증했다. readOnly prop으로 BlockRenderer를 공유하면 DOM은 editor와 같고 번들만 달라진다. npm publish exports/files에 CSS entry 누락은 소비자 unstyled viewer로 이어지므로 manifest 검증이 필수다. 결과적으로 peek/embed 경로에서 편집기 코드가 빠져 번들이 가벼워졌고, CI로 0건이 고정됐다.
교훈
- 포트폴리오 데모 = 블록 + 표면 + 번들 경계 — 셋 중 하나만 맞추면 설득력이 안 생긴다.
- peek는 같은 editor DOM, 다른 import graph — tree-shaking은 export 경계 문제다.
→ Read-only viewer bundle splitting
3. 표면·아카이브·embed·shell 정리
배경지식 (개념)
- seeded gradient 표면: 제목 해시를 시드로 파스텔 gradient cover를 생성하는 방식. cover 이미지 없이도 카드가 완성돼 보이게 한다.
- characterization 테스트: 기존 동작을 있는 그대로 고정해 리팩터 후 회귀를 잡는 테스트.
어떤 상황이었나
블록 타입은 늘었는데 카드 cover가 비어 있으면 허접한 그리드처럼 보인다. 또한 hook에 handler·patch·autoscroll 로직이 섞여 있어 characterization 테스트 없이는 IME·clipboard 회귀가 잦다. document 안에 SDUI, SDUI 안에 document를 서로 embed하면 순환이 생길 수 있다.
핵심 작업
- Surface: seeded gradient 표면 + 모션 축소 상태로, 허접한 그리드를 스크린샷 회귀로 고정된 카드로 바꿨다.
- Embed bridge: 단방향 variable bridge로 embed를 연결해 순환 없이 양쪽 embed가 성립하게 했다.
- Shell 정리: pure 함수 추출 후 characterization 테스트로 관련 패키지 전 suite green을 유지했다.
교훈
- 표면 시스템(gradient cover) 없이 블록만 예쁘게는 데모 설득력이 부족하다.
- imperative shell 정리는 pure 함수 추출 + characterization 순 — hook 비대화를 먼저 막는다.