정의
편집 가능한 영역(contenteditable — 사용자가 직접 글을 고칠 수 있게 만든 HTML 영역) 안에서 리치텍스트 엔진(ProseMirror, TipTap 같은, 굵게·목록 등을 다루는 편집기 라이브러리)을 쓸 때, Ctrl+Z(실행 취소)를 눌렀는데 편집기 자체의 되돌리기가 다 소진되면 그 다음 취소를 문서 전체 되돌리기로 넘겨주는 키보드 처리 패턴이다.
핵심 배경: 이런 편집기는 포커스(입력 커서가 들어가 있는 상태)일 때 Mod-Z/Mod-Y(맥은 Cmd, 윈도우는 Ctrl — 실행 취소/다시 실행 단축키)를 받으면, 브라우저 기본 undo가 끼어들지 못하도록 항상 preventDefault(이벤트의 브라우저 기본 동작을 취소하는 호출)를 건다. 그래서 편집기의 되돌리기 기록(undo stack — 되돌릴 수 있는 편집 단계를 쌓아둔 목록)이 비어도 키 이벤트는 이미 "처리됨"으로 표시된다. 그러면 상위 코드로 그냥 흘려보낼(bubble — 이벤트가 부모 요소로 전파되는 것) 수 없으므로, 명시적으로 문서 수준 되돌리기 핸들러를 호출해 넘겨야 한다.
왜 필요한가
이중 히스토리(편집기 안쪽 되돌리기 vs 문서 전체 되돌리기)를 설계할 때 흔히 하는 잘못된 가정이 있다: "편집기가 처리 못 하면 이벤트가 취소 안 된 채(unprevented) 부모로 올라오겠지"라는 것이다.
실제 브라우저에서는 편집기가 브라우저 기본 undo를 막으려고 되돌릴 게 없어도 Mod-Z에 preventDefault를 건다. 그래서 상위 핸들러가 if (event.defaultPrevented) return(이미 처리된 이벤트면 무시)처럼만 짜여 있으면, 조건이 항상 참이 되어 문서 수준 되돌리기가 영영 실행되지 않는다. 이걸 막으려면 "부모로 올라오는지"에 기대지 말고, 편집기 되돌리기가 실패한 순간 문서 되돌리기를 직접 불러야 한다.
동작 원리
- Guest keymap:
Mod-z→chainCommands(editorUndo, delegateDocumentUndo). editorUndo가 true면 인라인 history 소비.- false면
delegateDocumentUndo가 document history step 실행. - delegate 전 guest commit + retire로 텍스트 유실·stale commit 방지.
- Document history는 별도 stack(예: sidebar move vs 본문)일 수 있음.
실무 적용
ProseMirror 예:
회귀 테스트는 Playwright로: focus block → structural command(indent/move) → Mod-Z → DOM order 복원 assert. jsdom은 PM의 protective preventDefault를 재현하지 않아 false negative/positive가 난다.
디버깅 시 가설(focus loss, phantom history)은 추측 수정 전 document.activeElement, PM eventCount, defaultPrevented를 계측해 기각한다.
트레이드오프
- explicit chain은
defaultPreventedheuristic보다 장황하지만 브라우저별 일관. - delegate 전 commit은 latency 추가 — IME composing gate와 함께 설계.
- 다층 history(3+)는 keymap 우선순위 문서화 필수.
사용하면 안 되는 경우
- 단일 monolithic document PM 하나만 있고 document-level history가 없는 경우 — PM undo만으로 충분.
- global capture listener로 Mod-Z를 가로채 PM과 경쟁하는 경우 — 우선순위 충돌 별도 해결 필요.
흔한 실수
- selection keyboard handler의 blanket
defaultPreventedguard. - jsdom green으로 “undo OK” 판정.
- delegate without commit — block-level undo 후 텍스트 유실.
- inline/document history를 한 stack에 섞어 sidebar move까지 Ctrl+Z로 rollback.
관련 개념
- two-stack-inverse-undo — inverse stack 모델
- focused-guest-editor — guest/host history 층
- ime-composition-contenteditable — focus 중 다른 입력 경로