일이 지났습니다. 시의성에 유의하세요
가끔 밤에 침대에 누워서, 미약한 수입만으로 더 많은 책임을 지기 위한 새로운 방법을 열정적으로 찾곤 합니다. 그러다 문득 또 다른 오픈소스 프로젝트를 시작해야겠다는 생각이 들죠!
물론 위의 상황은 실제로 일어난 건 아니지만, 결과는 같습니다: 저는 계속해서 복잡하고 난이도 높은 코드를 구축하다가 포기하곤 합니다. 실제로 이 과정의 메커니즘은 대체로 어떤 기술적 개념을 먼저 떠올린 다음, 조사해 보니 아직 누구도 시도하지 않은 것을 발견하고, 결국 호기심과 자기실현 욕구를 충족시키기 위해 내가 할 수 있는지 확인해보기로 결정하는 것입니다.
이러한 메커니즘으로 인해 최근에는 이 ‘재앙’(물론 포기할 생각은 없습니다)이 탄생했습니다: ProseMirror라는 브라우저 기반의 리치 텍스트 에디터입니다. 저는 크라우드펀딩을 통해 이를 오픈소스로 공개했으며, 출시 후 유지보수를 어떻게 지속할지 고민해 보았습니다.
에디터라고요?
방금 전까지 ‘아직 아무도 하지 않은’ 일을 해야 한다고 말하지 않았나요? 현재 수백 개의 브라우저 기반 리치 텍스트 에디터가 존재하지 않나요?
네, 맞습니다. 하지만 기존 프로젝트 중 제가 이상적이라고 생각하는 접근 방식을 취한 것은 없었습니다. 많은 프로젝트가 contentEditable 요소에 의존한 후 발생하는 혼란을 해결하려는 오래된 패턴에 깊이 뿌리를 두고 있습니다. 이는 사용자와 브라우저가 우리의 문서에 대해 무엇을 하는지 거의 통제할 수 없게 만듭니다.
무엇을 통제해야 할까요? 첫째, 리치 텍스트 에디터는 문서를 합리적인 상태로 유지하기 쉽게 해야 합니다. 문서가 오직 여러분의 코드에 의해 수정된다면, 이러한 수정을 정의하여 유지하고 싶은 불변성을 보존할 수 있으며, 다른 브라우저에서도 동일한 일이 발생하도록 할 수 있습니다.
더 중요한 것은, 단순한 상태 변경이 아닌 더 추상적인 방식으로 이러한 수정을 표현할 수 있다는 점입니다: “여기서 어떤 변화가 발생했고, 따라서 새로운 문서가 생겼다.” 수정을 추상적으로 표현하는 것은 협업 편집 시 매우 유용합니다. 여러 사용자의 충돌하는 수정을 효과적으로 병합하는 데 도움이 되며, 수정의 _의도_를 정확히 표현할 수 있습니다.
기본 구현 방안
ProseMirror는 실제로 contentEditable 요소를 생성하여 그 안에 문서를 표시합니다. 이를 통해 포커스와 커서 이동과 관련된 모든 논리를 자유롭게 조작할 수 있으며, 스크린 리더와 양방향 텍스트 지원을 더 쉽게 할 수 있습니다.
문서에 가해진 실제 수정은 적절한 브라우저 이벤트를 처리하여 포착하고, 이를 우리만의 수정 표현으로 변환합니다. 비교적 현대적인 브라우저에서는 대부분의 유형의 수정을 추상적으로 설명하기 쉽습니다. 키 입력 이벤트를 처리하여 입력된 텍스트와 백스페이스, 엔터 같은 것을 포착할 수 있습니다. 클립보드 이벤트를 처리하여 복사, 잘라내기, 붙여넣기가 정상적으로 작동하도록 할 수 있습니다. 드래그 앤 드롭도 이벤트를 통해 구현됩니다. 심지어 IME 입력도 비교적 사용 가능한 조합 이벤트를 트리거할 수 있습니다.
안타깝게도 어떤 경우에는 브라우저가 사용자의 의도를 설명하는 이벤트를 트리거하지 않고, 단지 사후 输入 이벤트의 결과만 얻을 수 있습니다. 예를 들어, 맞춤법 검사 메뉴에서 수정을 선택할 때나, 리눅스에서 "Multi + e + ="를 사용해 "€"를 입력하는 것과 같은 조합 키로 특수 문자를 입력할 때 이런 일이 발생합니다. 다행히 지금까지 제가 마주한 모든 경우는 단순한 문자 수준의 입력만 관련되었습니다. 우리는 DOM을 검사하고, 문서에 대한 우리의 표현과 비교하여 예상되는 수정을 도출할 수 있습니다.
수정이 발생하면 에디터의 문서 표현이 변경되고, 화면의 DOM 요소가 새로운 문서를 반영하도록 업데이트됩니다. 문서에 지속적인 데이터 구조를 사용함으로써(수정이 이전 객체를 변경하지 않고 새로운 문서 객체를 생성하도록), 실제로 필요한 DOM 업데이트만 수행하는 매우 빠른 문서 차이 알고리즘을 사용할 수 있습니다. 이는 React와 그 다양한 파생 제품들이 하는 것과 유사하지만, ProseMirror는 일반적인 DOM-like 데이터 구조 대신 자체적인 문서 표현을 사용합니다.
에디터 문서
이 문서 표현은 물론 HTML이 아닙니다. 그러나 이는 문서의 ‘의미론적’ 표현입니다: 단락, 제목, 목록, 강조, 링크 등으로 텍스트 구조를 설명하는 트리 형태의 데이터 구조입니다. 이것은 DOM 트리로 렌더링되거나 Markdown 텍스트로 표현될 수 있으며, 그 외에도 인코딩된 개념을 표현할 수 있는 어떤 형식이든 가능합니다.
이 표현의 외부 계층, 즉 단락, 제목, 목록 등에 대한 처리는 구조적으로 DOM과 매우 유사합니다. 이는 자식 노드를 가진 노드들로 구성됩니다. 단락 노드(및 제목 같은 다른 블록 레벨 요소)의 내용은 각각 관련 스타일 집합을 가진 인라인 요소의 평면 시퀀스로 표현됩니다. 이는 DOM 같은 트리 구조를 전면적으로 사용하는 것보다 낫습니다. 이렇게 하면 강조 태그로 텍스트를 두 번 감싸는 것을 허용하지 않는 것과 같은 불변 부분을 추적하기 더 쉬워지며, 단락 내 위치를 트리 내 위치보다 추론하기 쉬운 간단한 문자 오프셋으로 표현할 수 있습니다.
단락 외부에서는 트리 구조를 사용해야 합니다. 따라서 문서 내 위치는 경로로 표현되며, 이는 트리의 각 레벨에서의 자식 인덱스와 이 경로 끝에 있는 노드의 오프셋을 나타내는 정수 시퀀스입니다. 이것이 커서 위치를 표현하는 방법이며, 수정이 발생한 위치를 기록하는 방법입니다.
ProseMirror의 현재 문서 모델은 Markdown 모델을 반영하며, 해당 형식으로 표현할 수 있는 것을 완벽하게 지원합니다. 앞으로는 특정 에디터 인스턴스에서 사용하고 싶은 문서 모델을 확장하고 맞춤 설정할 수 있을 것입니다.
사용자 인터페이스
현재 시중에 나와 있는 편집기에는 두 가지 스타일의 사용자 인터페이스가 있습니다. 하나는 상단에 위치한 클래식 툴바이고, 다른 하나는 선택 영역 위에 인라인 스타일을 설정하기 위한 툴팁을 표시하고, 현재 선택한 단락 오른쪽에 블록 레벨 작업을 위한 메뉴 버튼이 있는 방식입니다. 저는 후자를 선호하는데, 사용하지 않을 때 완전히 사라지지 않기 때문입니다(문서에 전혀 영향을 주지 않습니다). 하지만 많은 사람들이 익숙한 툴바를 더 선호할 것 같습니다.
이러한 사용자 인터페이스들은 모두 편집기 코어 외부의 모듈로 구현되었으며, 동일한 API를 기반으로 다른 스타일의 인터페이스도 구현할 수 있습니다.
키 바인딩도 구성 가능하며, CodeMirror의 패턴을 따릅니다. 키에 바인딩된 기능은 "명령"이라는 방식으로 사용할 수 있을 뿐만 아니라 execCommand 메서드를 통해 스크립트에서도 실행할 수 있습니다.
마지막으로 inputrules라는 모듈이 있는데, 이는 주어진 패턴과 일치하는 텍스트를 입력할 때 발생해야 할 동작을 지정하는 데 사용할 수 있습니다. “스마트 따옴표” 같은 기능이나 "1."을 입력하고 스페이스바를 눌렀을 때 목록을 생성하는 등의 용도로 활용할 수 있습니다.
협업 편집
앞서 협업에 대해 언급한 바 있습니다. 이 프로젝트에서 많은 노력을 기울인 부분이 바로 실시간 협업 편집을 지원하는 것입니다. 기술적 세부 사항에 대해 블로그 포스트]([중국어 번역](/tech/Collaborative-Editing-in-ProseMirror.html)]도 있습니다)를 별도로 작성했는데, 기본 개념은 다음과 같습니다:
문서를 수정할 때 새로운 문서와 함께 이전 문서의 위치를 새 문서로 매핑하는 위치 매핑이 생성됩니다. 예를 들어, 수정에 따라 커서를 이동시키는 경우가 이에 해당합니다.
위치를 매핑할 수 있다는 것은 다른 수정 사항을 기반으로 해당 위치를 매핑하여 수정을 "rebase"할 수 있다는 의미입니다. 이 외에도 여러 가지 고려 사항이 있어 시스템을 완성하기 위해 여러 번 재작성해야 했지만, 최종적으로 기대에 부합하는 코드를 얻었다고 확신합니다.
협업 시나리오에서는 클라이언트가 수정을 하면 로컬에서 버퍼링된 후 서버로 전송됩니다. 만약 다른 클라이언트가 우리의 수정이 도착하기 전에 자신의 수정을 전송하면, 서버는 "아니요, 먼저 이 수정 사항을 적용하세요"라고 응답합니다. 그러면 다른 클라이언트는 이 수정 사항을 수락하고, 이를 기반으로 자신의 수정을 재구성한 후 다시 시도합니다. 수정이 통과되면 다른 모든 클라이언트에 브로드캐스트되어 모든 사용자가 동기화 상태를 유지합니다.
대상 사용자
누가 ProseMirror를 사용하기에 적합할까요?
-
한편으로는 Markdown이나 유사한 형식으로 입력을 받는 웹사이트에서 기술적 배경이 부족한 사용자들을 위해 더 쉽게 배울 수 있는 인터페이스를 제공하고, 결과를 Markdown으로 변환하기를 원하는 경우입니다.
-
다른 한편으로는 기존의 전통적인 리치 텍스트 입력을 제공해왔지만 출력 내용을 제어하고 싶은 웹사이트에서는 ProseMirror로 전환하는 것이 좋습니다. 지저분한 HTML을 정리하고 최상의 결과를 바라는 것보다 편집 경험이 직접적으로 제약 사항을 반영하고 실행하는 편이 훨씬 낫기 때문입니다.
-
마지막으로, 협업 편집을 지원하면서 사용자들이 Google 문서 도구에서 하던 작업을 자사 제품으로 옮기고 싶은 기업들입니다.
흥미롭게 들리시나요? 이 오픈 소스 프로젝트의 크라우드펀딩 캠페인]이 어떻게 진행되고 있는지 확인해 보세요.
저는 인생의 중요한 선택의 기로에 섰을 때, 누군가 최선의 방법을 알려주어 소중한 시간을 헛되이 보내지 않기를 바라곤 합니다. 그런 마음으로 저는 자주 블로그를 쓰며, 광활한 인터넷의 이 작은 구석에 제게는 단 한 번뿐인 인생 경험을 기록하여 도움이 필요한 분들에게 도움이 되기를 바랍니다.