Milkdown 기반 마크다운 에디터에서 Plugin을 통해 joinTextblockBackward 시 heading의 bold mark가 paragraph로 누수되는 버그 해결
fixing two bugs stacked on top of each other in ProseMirror
AI 요약
Context
Markdown 에디터에서 heading의 bold 마크를 제거하고 Backspace로 상단 paragraph와 병합할 때, 해당 bold 마크가 이전 unstyled 텍스트로 누수되는 문제가 발생했다. 이는 ProseMirror의 joinTextblockBackward 함수가 mark를 유지하는 방식과 markdown 에디터가 heading의 시각적 속성을 node type과 inline mark 두 곳에 모두 저장하는 모델 설계의 충돌로 인한 것이다.
Technical Solution
- headingBackspacePlugin 생성: Plugin을 통해 joinTextblockBackward의 dispatch를 intercept하고 transaction 실행 전에 heading 내용 범위를 snapshot 처리
- ReplaceStep 매핑 추적: tr.mapping.map(prevEnd)를 사용해 block boundary 삭제 후 heading 콘텐츠가 paragraph 내에서의 새로운 위치 계산
- Mark 제거 루프: for 루프로 schema의 모든 mark type에 대해 tr.removeMark(from, to, markType) 호출하여 former-heading 범위의 모든 mark 제거
- Cursor mark 초기화: tr.setStoredMarks([])을 호출해 cursor가 다음 입력 문자에서 bold/italic mark를 상속받지 않도록 설정
- Atomic transaction 관리: join과 mark removal을 단일 transaction에 포함시켜 Ctrl+Z로 전체 작업을 원자적으로 취소 가능하게 구현
Key Takeaway
Markdown 에디터에서 block 병합 시 mark 누수 문제는 ProseMirror의 정상 동작이 아니라 markdown의 시각적 속성을 모델링하는 방식의 구조적 문제이므로, 에디터 프레임워크 레벨의 Plugin 계층에서 transaction dispatch를 intercept하여 선택적으로 mark를 제거하는 방식으로 해결할 수 있다.
실천 포인트
Milkdown이나 Tiptap 같은 ProseMirror 기반 마크다운 에디터를 구축할 때, heading node의 bold mark가 병합 후 paragraph로 누수되는 현상을 제거하려면 handleKeyDown에서 Backspace 이벤트를 감지한 후 wrappedDispatch를 통해 tr.mapping.map()으로 mark 위치를 재계산하고 모든 mark type에 대해 removeMark를 호출하는 Plugin을 프레임워크의 plugin API로 등록하면 된다.