정의
Notion처럼, 카드를 누르면 문서가 팝업(모달)으로 열리고, 그 안에서 링크를 또 누르면 팝업을 새로 겹치지 않고 같은 팝업의 내용만 다음 문서로 바뀌며 깊이가 쌓이는 탐색을 만들고 싶을 때 쓰는 패턴이다. "뒤로" 누르면 이전 문서로 돌아온다. 즉, 팝업 껍데기는 하나로 고정하고 그 안에 든 문서만 갈아 끼운다.
정확히 말하면, Nested modal document navigation은 overlay dialog(화면 위에 떠서 뒤 배경을 덮는 팝업 창) 하나를 유지한 채 내부에서 documentId(어떤 문서를 보여줄지 가리키는 식별자)만 바꾸며 깊이를 쌓는 UX 패턴이다. gallery card(목록의 카드)→peek(카드를 눌러 살짝 들여다보는 미리보기 팝업)→related page(그 문서가 참조하는 연관 문서)가 같은 shell(팝업 껍데기 컴포넌트) 연속성을 유지한다. 이때 열어 온 문서들을 순서대로 쌓은 것을 스택이라 하고, 새 문서로 들어가는 것을 push, 뒤로 나오는 것을 pop이라 한다.
왜 필요한가
문서 A를 팝업으로 열고, 그 안의 링크로 문서 B, 다시 문서 C로 파고드는 흐름을 소박하게 만들면 흔히 "링크마다 새 팝업/새 라우트(URL 경로)를 연다". 그러면 팝업이 여러 겹으로 겹치면서 다음 문제가 겹쳐 터진다.
- focus trap 중첩: focus trap은 팝업이 열려 있는 동안 키보드 포커스(Tab 이동)가 팝업 밖으로 새어 나가지 못하게 가두는 접근성 장치다. 팝업이 여러 겹이면 어느 층에 포커스를 가둘지 꼬인다.
- scroll lock 중첩: scroll lock은 팝업이 떠 있을 때 뒤 배경 페이지가 스크롤되지 않게 막는 처리다. 여러 겹이면 잠금·해제 타이밍이 어긋난다.
- 애니메이션 중첩과 spatial context(공간적 맥락, "내가 지금 어디에 있고 어떻게 여기 왔는지"의 감각) 단절: 매번 새 팝업이 위에서 튀어나오면 사용자는 계층을 잃는다.
Notion형 peek은 이 문제를 프레임(팝업 껍데기)은 고정, 내용만 push로 푼다. 껍데기가 하나뿐이니 focus trap·scroll lock·애니메이션은 딱 한 번만 걸리고, 사용자는 같은 창 안에서 앞뒤로 이동하는 일관된 감각을 얻는다.
동작 원리
- Modal 컴포넌트는 mount(화면에 붙은 상태)를 유지하고, 안에서
activeDocumentIdstate만 바꾼다. 껍데기는 다시 만들지 않으므로 focus trap·scroll lock·열림 애니메이션이 한 번만 실행된다. - 스택·breadcrumb(현재 위치까지의 경로 표시)와 URL 동기화(선택)는 navigator가 소유한다. 즉 "지금까지 어떤 문서를 거쳐 왔는지" 목록은 팝업 자신이 아니라 별도 navigator 계층이 들고 있어야, 뒤로 가기·경로 표시가 한곳에서 관리된다.
- readOnly(읽기 전용)에서 editable(편집 가능)로 승격(promotion)될 때는
onContentChange콜백으로 변경을 상위에 persist(저장)한다. 미리보기로 들어왔다가 편집을 시작하면 그 편집이 유실되지 않게 하는 지점이다. - 비동기 resolve(문서 데이터를 서버·캐시에서 불러와 확정하는 과정) 중간 프레임 차단: 다음 문서
newId를 요청해 데이터가 도착하기 전까지, 이전 문서oldDoc와 뒤섞인 어정쩡한 중간 화면이 잠깐 보이지 않도록 렌더를 막는다.
실무 적용
- refresh 모드에서 peek 열기: 평소엔 캐시에 있으면 바로 쓰는 cache-first가 빠르지만, "새로고침" 의도로 열 때는 캐시를 건너뛰고 force network(무조건 서버에서 새로 가져오기)로 최신본을 보장한다.
- Playwright(브라우저 자동화 E2E 테스트 도구) drag 실패는 simulation baseline을 분리한다. 드래그 같은 물리 시뮬레이션은 실제 환경과 테스트 환경의 결과가 달라지기 쉬우므로, 테스트 기준선을 따로 두어 오탐을 줄인다.
- RELATED(연관 문서) 링크는 새 탭이 아니라 in-modal push로 처리한다. 같은 팝업 안에서 다음 문서로 이어져야 스택 연속성이 유지된다.
트레이드오프
- 스택 깊이 상한(depth cap)과 모바일 뒤로 가기 제스처 정책이 필요하다. 무한히 파고들게 두면 사용자가 몇 겹 들어왔는지 잃고, 모바일에서 스와이프 뒤로 가기와 팝업 pop이 충돌한다.
- popstate(브라우저 뒤로/앞으로 버튼이 눌릴 때 발생하는 이벤트)/back 버튼은 view transition(화면 전환 애니메이션 API)과 궁합이 나쁘다. 브라우저 히스토리에 의존하면 전환 애니메이션과 어긋나므로, 필요하면 URL을 명시적으로 push해 스택과 히스토리를 직접 맞춘다.
사용하면 안 되는 경우
- 카드 그리드가 얕고(shallow) related depth가 없어서 "카드 → 문서 하나"로 끝나면, 스택을 쌓을 일이 없으니 그냥 single-level modal(한 겹짜리 팝업)로 충분하다. 굳이 스택·navigator를 만들 필요가 없다.
흔한 실수
useResolvedDocument(현재 documentId로 문서 데이터를 확정해 주는 훅)가 stale(오래된 값): 다음 문서로 push했는데 render 시점 reset을 빠뜨려 이전 문서 데이터가 그대로 남는다. documentId가 바뀌면 렌더 시점에 파생 상태를 초기화해야 한다.- reopen(다시 열기) 시 롤백: 편집 후 닫았다가 다시 열었을 때 cache-first가 옛 캐시를 보여 줘 편집이 사라진 것처럼 보인다. 재열기 경로의 캐시 신선도 정책을 명확히 한다.
관련 개념
- hierarchical-document-navigation — 문서 계층을 따라 이동하는 상위 탐색 모델
- fixed-position-floating-ui-scroll — 팝업 안 플로팅 UI가 스크롤과 어긋나지 않게 하는 위치 처리