정의
노션·위키처럼 "문서 안에 다른 문서가 들어 있고, 그걸 눌러 계속 파고드는" UI를 만들고 싶을 때 쓰는 패턴이다. Hierarchical document navigation(계층형 문서 탐색)은 하나의 평평한(flat) 문서가 아니라, 문서들이 서로를 참조하는 그래프를 다룬다. page 블록(문서 안에서 다른 문서를 가리키는 블록)과 navigator(탐색 조작을 담당하는 인터페이스: push=안으로 들어가기, peek=미리보기 창으로 엿보기, back=뒤로 가기)로 자식 문서를 오간다. breadcrumb("홈 > 프로젝트 > 세부" 같은 현재 위치 경로 표시)은 지금까지 들어온 스택을 반영하고, peek dialog(엿보기용 모달 창)는 readOnly editor surface(수정 불가 모드의 에디터 화면)를 재사용한다.
맥락에서 이해하기
블로그 글 하나처럼 문서가 한 겹이면 URL 라우팅만으로 충분하다. 하지만 "프로젝트를 열면 그 안에 하위 문서가 있고, 그걸 또 열면 더 깊은 문서가 나오는" 구조에서는, 매번 페이지를 갈아끼우면 맥락이 끊긴다. 이 패턴은 문서를 id로 연결된 그래프로 보고, 같은 모달 껍데기 안에서 내용 id만 바꿔 끼우거나(peek), 스택에 쌓아 깊이 들어갔다가(push) 다시 pop해 나오는(back) 방식으로 탐색 연속성을 유지한다. "브라우저 뒤로 가기"가 아니라 앱이 직접 관리하는 명시적 스택이 핵심이다.
왜 필요한가
위키·포트폴리오·KB 데모는 "홈→프로젝트→세부" 깊이가 있어야 설득력이 생긴다. URL route만으로는 overlay peek·in-place push 연속성을 만들기 어렵다.
동작 원리
- Page block이
documentId를 참조한다 — 링크(route)와 임베드(in-place)를 구분한다. - Navigator가
{ push, peek, back }계약을 제공한다 — React context가 아니라 주입 가능한 인터페이스로 두면 Storybook·테스트에서 in-memory graph를 쓸 수 있다. - Peek는 modal shell을 유지하고 내부 id만 교체한다 — readOnly editor surface 재사용으로 markup drift를 막는다.
- Archive는 applyPatches 체크포인트에서 서브트리 page id를 수집해 repository에 전달한다 — “블록 삭제”와 “자식 문서 정리”를 한 트랜잭션으로 묶는다.
- Document resolver는 id별 fetch dedup + (newId, oldContent) 중간 프레임 차단(render-time reset 또는
key={id})을 한다.
실무 적용
- navigator를 injectable interface로 두어 Storybook·테스트에서 in-memory graph 사용.
- slash command "Page"는 capability gate 뒤에 둔다.
- create 성공 후 block patch와 document create를 한 사용자 액션으로 묶는다.
트레이드오프
- in-modal push는 route VT와 별개 —
router.back()대신 explicit stack pop. - resolver cache는 성능↑ stale bug↑ — id 변경 시 key reset 필수.
사용하면 안 되는 경우
- 단일 페이지 블로그처럼 depth=1이면 page block·peek는 과하다.
흔한 실수
- peek마다 새 Dialog mount → animation/focus 깨짐.
- refresh cache-first로 reopen 시 이전 revision 표시.
관련 개념
- focused-guest-editor — peek 편집 surface
- radix-portal-scoped-css — dialog 스타일 격리