협업 텍스트 편집: CRDT나 OT 없이 구현하기
(mattweidner.com)- 중앙 서버 기반 협업 앱에서 텍스트를 배열 인덱스로 편집하면 동시 편집 때 위치가 밀리므로, 각 문자에 전역 고유 ID를 붙이고 “특정 ID 뒤에 삽입”하는 방식으로 서버 상태를 갱신함
- 실제 서비스에서 쓰이는 CRDT와 OT는 강력하지만, 총순서 알고리듬이나 연산 변환 규칙이 복잡해 앱 요구에 맞춰 내부 동작을 바꾸기 어려움
- 제안 방식은 클라이언트와 서버가
Array<{ id: ID; char?: string; isDeleted: boolean }>형태의 ID 목록을 유지하고, 삭제된 문자도 tombstone처럼 남겨 이후 삽입 위치 참조가 깨지지 않게 함 - 낙관적 로컬 업데이트는 서버 조정(server reconciliation) 으로 처리하며, 원격 연산을 받을 때 대기 중인 로컬 연산을 되돌린 뒤 원격 연산과 미승인 로컬 연산을 순서대로 다시 적용함
- 동시 삽입 순서, 리치 텍스트 포맷, 분산형 변형,
Articulated라이브러리까지 다루며, 서버가 앱별 의미에 맞춰 삽입·삭제를 넘어선 유연한 연산을 정의할 수 있음
인덱스 기반 편집이 동시 편집에서 깨지는 이유
- 협업 텍스트 편집에서 클라이언트는 사용자가 입력한 연산을 서버로 보내고, 서버는 자신의 권위 있는 상태를 갱신해야 함
- 텍스트를 문자 배열로 보고
index 17에 " the" 삽입같은 연산을 보내면, 서버 도착 전 다른 사용자의 삽입 때문에 같은 인덱스가 다른 위치를 가리킬 수 있음- 예를 들어 Alice가 앞쪽에
" gray"를 삽입하면 Bob의index 17은 더 이상 원래 위치가 아님 - 서버는 Bob의 연산을
index 22로 리베이스해야 함
- 예를 들어 Alice가 앞쪽에
- 핵심은 클라이언트가 어떤 연산을 서버에 보내고, 서버가 이를 어떻게 해석해야 텍스트를 “명백히 올바른” 방식으로 갱신할 수 있느냐임
- 이 인덱스 리베이스 문제는 Google Docs 같은 실시간 협업 앱뿐 아니라, 리스트 항목을 삽입하는 웹 폼이나 인라인 댓글·편집 기록을 다루는 단일 스레드 로컬 앱에서도 나타날 수 있음
CRDT와 OT가 실무에서 부담스러운 지점
- 기존 해법은 크게 CRDT와 OT로 나뉨
- CRDT는 각 문자에 불변 ID 또는 “position”을 부여하고, 특수한 트리 순회 같은 수학적 총순서로 ID를 정렬함
- OT는 동시 편집을 고려해 연산 자체를 변환하며, 예시에서는
index 17 삽입을index 22 삽입으로 바꿈
- 두 접근은 이미 실제 서비스에서 사용됨
- Google Docs는 OT를 사용함
- Yjs CRDT 라이브러리는 여러 앱에서 활용됨
- 부담은 개념적 복잡성에서 생김
- 텍스트 편집 CRDT의 총순서는 학술 논문에 정의된 미묘한 알고리듬인 경우가 많음
- OT 알고리듬은 대수적 “변환 속성”을 만족해야 하며, 경우의 수가 제곱으로 늘고 형식 검증 없이는 결함이 잦음
- 복잡한 알고리듬은 구현도 복잡하게 만들고, 보통 전문가가 만든 라이브러리를 네트워크 블랙박스처럼 사용하게 됨
- 라이브러리가 예상하지 못한 기능이 필요할 때 단일체적 구조가 발목을 잡음
- 큰 문서의 필요한 부분만 메모리에 올리고 나머지는 디스크에 두기
- 문단별 편집 권한이나 특정 포맷 사용 권한 같은 하위 문서 권한을 서버에서 강제하기
- Google Docs 스타일의 제안 변경을 본문 안이나 옆에 표시하기
- Replicache 같은 키-값 저장소와 동기화하기 쉬운 표현으로 텍스트 저장하기
- 삽입·삭제 외에 텍스트 이동, 문서 트리 조작, 문단 분할·병합 같은 연산 지원하기
문자 ID와 “insert after” 방식
- 기본 아이디어는 배열 인덱스 대신 각 문자에 전역 고유 ID를 붙이는 것임
- 핵심 자료구조는
Array<{ id: ID; char: string }>형태임 - 클라이언트는
index 17에 삽입대신f1bdb70a 뒤에 " the" 삽입같은 연산을 서버에 보냄 - 서버는 대상 ID를 찾아 그 바로 뒤에 새 문자를 넣음
- 핵심 자료구조는
- 새 문자들의 ID도 클라이언트가 함께 지정해야 함
- 예:
f1bdb70a 뒤에 " the"를 ids [...]로 삽입 - 클라이언트가 ID를 생성하면 서버 응답을 받기 전에 후속
insert after연산에서 새 ID를 참조할 수 있음
- 예:
- 삭제된 문자를 완전히 제거하면 삽입 위치를 잃을 수 있음
- Bob이
26085702뒤에 삽입하려는 동안 다른 사용자가26085702문자를 삭제하면, 서버는 어디에 삽입해야 할지 알 수 없음 - 서버는 삭제된 ID도 내부 목록에 유지해야 함
- Bob이
- 보정된 상태 표현은 다음과 같음
Array<{ id: ID; char?: string; isDeleted: boolean }>
- 사용자에게 보이는 텍스트는 삭제되지 않은 항목만 이어 붙여 만들 수 있음
list.filter(elt => !elt.isDeleted).map(elt => elt.char).join('')
삽입과 삭제 처리
- 문자 입력 시 클라이언트와 서버의 동작은 단순함
- 클라이언트는 삽입 지점 바로 앞 문자의 ID인
before를 찾음 - 새 문자에 대해 UUID 같은 전역 고유 ID
id를 생성함 - 서버에
before뒤에char를id로 삽입하라는 연산을 보냄 - 서버는 삭제된 항목까지 포함해
before를 찾고, 그 항목 바로 뒤에{ id, char, isDeleted: false }를 삽입함
- 클라이언트는 삽입 지점 바로 앞 문자의 ID인
- 문자 삭제도 ID 기반으로 처리함
- 클라이언트는 삭제할 문자의
id를 찾음 - 서버에 해당 ID의 항목을 삭제하라는 연산을 보냄
- 서버는 해당 항목을 찾아 아직 삭제되지 않았다면
entry.isDeleted = true로 설정함
- 클라이언트는 삭제할 문자의
- 이 방식은 CRDT나 OT 논문을 따라가지 않고도 서버로 보내는 편집 연산의 위치 문제를 직접 해결함
- 단순 배열 구현은 문자마다 UUID를 저장해야 하므로 비효율적일 수 있으며, 최적화는
Articulated에서 다룸
낙관적 업데이트와 서버 조정
- Google Docs 스타일의 협업 편집에서는 사용자가 서버 응답을 기다리지 않고 자신의 입력 결과를 즉시 봐야 함
- 어려운 지점은 클라이언트에 아직 서버가 승인하지 않은 대기 중인 로컬 연산이 있는 상태에서, 그와 동시인 원격 연산을 서버로부터 받을 때임
- 이 경우 CRDT가 꼭 필요한 것은 아니며, 서버 조정(server reconciliation) 으로 처리할 수 있음
- 대기 중인 모든 로컬 연산을 되돌려 클라이언트 상태를 이전 서버 상태 관점으로 되감음
- 원격 연산을 적용해 클라이언트를 서버 상태에 맞춤
- 아직 승인되지 않은 로컬 연산을 다시 적용함
- 더 단순한 전략으로는 대기 중인 로컬 연산이 있을 때 원격 연산 처리를 금지하는 Wait for Ack가 있음
- Bob의 클라이언트는 자신의 메시지가 처리된 서버 상태를 받을 때까지 첫 서버 메시지를 무시할 수 있음
- Bob이 계속 입력하거나 네트워크 지연이 크면 지연이 무한히 길어질 수 있어 서버 조정보다 덜 실시간적임
CRDT와 달라지는 부분
- 제안 방식은 문자마다 ID를 붙이고
isDeleted표시를 사용한다는 점에서 CRDT와 일부 특징을 공유함 - 차이는 순서를 다루는 방식에 있음
- 이 방식에서는 클라이언트가
X를 Y 뒤에 삽입하라고 서버에 말하고, 서버는 그대로 하거나 개발자가 정의한 다른 방식으로 처리함 - 텍스트 편집 CRDT에서는 ID가 복잡한 알고리듬에 의해 정렬됨
- 이 방식에서는 클라이언트가
- 여러 텍스트 편집 CRDT 사이의 차이를 만드는 핵심도 이 ID 정렬 알고리듬이며, 이 접근은 그 부분을 피함
동시 삽입이 만드는 결과
- 같은 위치에 여러 사용자가 동시에 입력하면 서버가 연산을 받은 순서의 역순으로 결과가 배치됨
- 예를 들어 텍스트가
"My name is"이고 Charlie가" Charlie", Dave가" Dave"를 동시에 입력한다고 가정함- Charlie의 연산이 먼저 도착하면 서버는
"My name is Charlie"를 만듦 - Dave의 연산도 같은
is의sID 뒤에 삽입하므로 결과는"My name is Dave Charlie"가 됨
- Charlie의 연산이 먼저 도착하면 서버는
- 같은 대상 ID 뒤에 대한
insert after연산은 동시성이 없더라도 서버 수신 순서의 역순이 됨 - 그래도 왼쪽에서 오른쪽으로 입력한 단어들은 문자 단위로 뒤섞이지 않음
- Dave가 각 문자를 별도 연산으로 보내도
a는D뒤,v는a뒤에 삽입됨 - 서버 상태는
"My name is D Charlie"→"My name is Da Charlie"→"My name is Dav Charlie"→"My name is Dave Charlie"처럼 변함
- Dave가 각 문자를 별도 연산으로 보내도
- 오른쪽에서 왼쪽으로 입력하는 경우에는 Charlie와 Dave의 연산이 교차 순서로 서버에 도착하면 결과 텍스트도 교차될 수 있음
- 실제로는 두 사용자가 동시에 온라인이고 서로의 진행 중 편집을 무시할 때 발생할 수 있음
서버가 더 유연한 연산을 정의할 수 있음
- 서버 조정을 사용하면 서버는 클라이언트 연산을 사실상 원하는 방식으로 처리할 수 있고, 클라이언트는 결국 같은 상태에 도달함
- 이는 엄격한 대수 규칙을 만족하는 연산만 허용하는 CRDT·OT와 대비됨
- 같은 위치의 동시 삽입에 대해 서버는 여러 방식으로 대응할 수 있음
- 해당 연산을 무시해 no-op으로 처리
- ID는 내부 목록에 추가하되 즉시 삭제 표시해, 이후 Dave의 연산이 이전 ID를 참조할 수 있게 함
- 텍스트를 삽입하되 두 단어에 검토용 특수 포맷을 적용
- Dave의 편집을 본문 옆에 표시되는 “제안”으로 변환
- LLM에 텍스트를 어떻게 고칠지 물어봄
- 클라이언트는 사용자 의도를 더 잘 담는 연산을 보낼 수도 있음
insert before는 문단 위에 제목을 만들 때 이전 문단 끝의 동시 삽입 중간에 제목이 들어가는 것을 피하는 데 쓸 수 있음fix typo연산은ID X를 가진 color의 o 뒤에 u를 삽입하되, 주변 단어가 여전히 color일 때만같은 조건을 담을 수 있음
- 서버는 삽입 위치 자체가 서버 도착 후 달라지는 연산도 정의할 수 있음
- 같은 위치의 동시 삽입을 알파벳순으로 재정렬할 수 있음
- 드래그앤드롭용
move연산을 추가하면, 이동된 텍스트 내부의insert after를 원래 위치가 아니라 이동된 텍스트 내부에 적용할 수 있음
리치 텍스트 포맷 처리
- 리치 텍스트에서는 굵게, 글자 크기, 하이퍼링크 같은 인라인 포맷을 다룸
- 범위 포맷도 인덱스 대신 문자 ID로 표현할 수 있음
- 예:
ID X부터 ID Y까지 bold 적용 ID X inclusive부터 ID Y exclusive까지로 정의하면 범위 끝의 동시 삽입도 굵게 처리할 수 있음
- 예:
- ProseMirror 같은 리치 텍스트 편집기와 함께 쓰면 서버는 ID X와 Y의 현재 배열 인덱스를 찾아, 로컬 ProseMirror 상태에 해당 범위를 굵게 처리하라고 지시할 수 있음
- ProseMirror는 이후 해당 범위 안에 삽입되는 텍스트에도 굵게를 유지할 수 있음
- 단 서버가
bold set to false같은 삽입 연산에 따라 다르게 처리할 수도 있음
- 단 서버가
- 협업 리치 텍스트의 의미론을 이해하려면 Peritext essay가 참고 자료가 됨
분산형 변형과 CRDT와의 연결
- 지금까지는 중앙 서버가 연산의 총순서를 서버 수신 순서로 정하고, 권위 있는 상태를 갱신한다고 가정함
- 중앙 서버가 없거나 서버가 선택적인 앱에서는 연산에 대한 최종적 총순서를 분산 방식으로 부여할 수 있음
- 예를 들어 Lamport timestamps로 연산을 정렬함
- 각 클라이언트는 지금까지 받은 연산을 순서대로 처리한 결과를 권위 있는 상태로 삼음
- 이 경우 문자별 ID와
insert after연산은 분산형 “비서버” 조정에서도 동작함 - 기술적으로는 이 결과가 텍스트 편집 CRDT가 됨
- 분산형이고, 최종 일관성을 갖는 협업 텍스트 편집 알고리듬이기 때문임
- 어떤 순서 방식을 쓰느냐에 따라 기존 CRDT와 연결됨
- 이 동등성 주장에 대한 자세한 증명은 작성되어 있지 않음
Articulated: 구현 보조 라이브러리
- 실제 구현에서는 텍스트 자체를 ProseMirror 상태 같은 다른 곳에 저장하고, 이 접근에는 다음 형태의 ID 목록만 필요할 수 있음
Array<{ id: ID; isDeleted: boolean }>
- 이 목록에서 자주 필요한 작업은 네 가지임
- ID와 현재 배열 인덱스 사이 변환
- 지정한 ID 뒤에 새 ID 삽입
- ID를 삭제 표시
- 저장을 위해 상태를 직렬화하고 복원
- 단순 배열은 이 작업들에 적합하지 않음
- 작업 1~3은 선형 시간이 걸림
- 문자마다 객체와 UUID를 저장하므로 메모리와 저장 공간이 큼
- Articulated는 이 배열과 같은 기능을 제공하는 작은 npm 라이브러리임
- 핵심 자료구조
IdList는 인기 있는 텍스트 편집 CRDT 라이브러리와 비슷한 최적화를 사용함- ID는
{ bunchId, counter }형태이며,bunchId는 여러 ID가 공유할 수 있는 UUID임 - 왼쪽에서 오른쪽으로 삽입하는 일반적인 경우처럼 같은 bunch의 ID가 나란히 있으면 메모리와 직렬화 상태에서 하나의 객체로 저장함
- 핵심 자료구조는 배열이 아니라 B+Tree라서 메서드 호출 시간이
log또는log^2임
- ID는
IdList는 영속 자료구조(persistent data structure) 이기도 함- 클라이언트는 서버에서 마지막으로 받은 상태와 낙관적 상태를 싸게 함께 저장할 수 있음
- 원격 연산을 받을 때 서버의 마지막 상태로 롤백하기 쉬움
- 추가 자료로 docs, 초기 demos, 300 SLOC 미만의 단순 구현인 IdListSimple이 제공됨
IdListSimple은 최적화와 영속성을 생략했지만 기능적으로 동일하며, fuzz tests로 검증됨
댓글과 토론
Hacker News 의견들
-
꽤 깔끔함. 알고리즘은 각 텍스트 문자에 UUID 같은 전역 고유 ID를 붙여, 계속 바뀌는 배열 인덱스 대신 시간에 걸쳐 일관되게 참조할 수 있게 하는 방식임
클라이언트는 기존 ID를 참조하는 “insert after” 연산을 서버로 보내고, 서버는 대상 ID를 찾아 새 문자를 바로 뒤에 삽입함. 삭제는 표시상 숨길 뿐 “insert after” 위치 계산을 위해 문자는 계속 보관함. 텍스트 편집 밖에서도, 예를 들면 게임 월드 동기화 같은 곳에 가능성이 있어 보임- 이건 말 그대로 퇴화된 CRDT임. 충돌 순서를 중앙 서버가 정하는 방식은 Google Wave 때부터 있었음
- 이게 정말 그렇게 새로운가 싶음. 분산 시스템을 직렬화하기 위해 중앙 프로세스를 쓰는 건 당연한 출발점에 가까운데, 네트워크 분할과 CAP 같은 걸 걱정하기 전까지는 그럼. 이제 단일 장애 지점도 생김. 대충 훑어봤는데 성능 얘기가 있었는지도 궁금함
- 설명한 건 CRDT 아닌가?
ctrl+a,ctrl+x,ctrl+v하면 행운을 빌어야 할 듯
-
이런 글을 보니 반가움. 몇 년 전에 같은 방법을 발견했고, 왜 학술 문헌에 잘 안 보이는지 궁금했음
다만 나는 이걸 탈중앙 맥락에서 CRDT로 구현해서 교환법칙, 멱등성, 결합법칙 같은 성질이 유지되게 했음- CRDT의 대안을 만들려는 아이디어라면, 그걸 CRDT로 만들면서 얻은 게 뭐였는지 궁금함
-
dict/map이나 임의 타입 배열 같은 다른 자료구조 얘기가 없어서 의외였음. 그런 것들도 쉽게 확장되면 좋겠음. 경험상 앱에는 순수한 공동 텍스트 편집보다 협업 자료구조가 더 자주 필요함
동기 예시인 업데이트 검증, 부분 로딩, 고수준 연산은 흥미롭지만, Yjs 같은 것에 이런 기능이 없는 이유가 기반 CRDT 구현 때문인지, 아니면 애초에 이런 기능을 만들기 어려워서인지는 설득력이 약해 보임- 완전히 동의함. 속성을 바꿀 수 없는 “원자적” 객체 배열이라면 문자열을 자기 타입으로 바꾸기만 해도 가능할 것 같음. 객체 내부 변경은 더 까다롭겠지만, 어쩌면 트리를 효율적으로 저장하고 순회하는 문제일 수도 있음
OP 용어로 말하면 헬퍼 라이브러리 사용자가 가벼운 의미 모델 로직을 끼워 넣어 잘못된 상태를 막거나 관리할 수 있어야 한다고 늘 생각했음. 예를 들어 할 일 항목이isDone: true이면서 동시에state: inProgress일 수는 없음. 링크된 글에서 말한 리치 텍스트 서식 의미론과 비슷함 - CRDT는 본질적으로 충돌이 생겼을 때 한쪽을 결정적으로 선택하는 방식으로 동작함. 문제는 일반적으로 이 방식이 데이터 손실이 없거나 데이터가 유효하다는 걸 보장하지 않는다는 데 있음
모든 Git 병합 충돌이 한쪽을 자동으로 고르는 식으로 해결된다고 상상해보면 됨. 대개 잘못된 결과가 나오고, 때로는 컴파일도 안 되는 코드가 될 수 있음. 그때 바로 고칠 사람이 없다면 더 혼란스러운 결과로 이어짐
그래서 CRDT가 더 널리 퍼지지 않았다고 봄. CRDT는 “있다고 생각한 문제”만 고치고, 실제 문제인 데이터와 유효성, 의미를 보존하는 충돌 해결은 해결하지 못함. 오히려 충돌 해결 방법을 결정적으로 복제 가능한 방식으로만 제한하기 때문에 이 문제를 더 악화시킨다고도 볼 수 있음
- 완전히 동의함. 속성을 바꿀 수 없는 “원자적” 객체 배열이라면 문자열을 자기 타입으로 바꾸기만 해도 가능할 것 같음. 객체 내부 변경은 더 까다롭겠지만, 어쩌면 트리를 효율적으로 저장하고 순회하는 문제일 수도 있음
-
CRDT와 다른 핵심은 중앙 서버가 있다면 동기화, 즉 동시 이벤트 사이의 순서 확정을 자료구조 자체가 사전 순서로 처리하지 말고 서버가 하게 하자는 점으로 보임
모든 통신이 클라이언트-서버 사이에서만 일어나고 클라이언트끼리는 일어나지 않으므로, 클라이언트가 서버에 연결될 때 서버는 새 원격 업데이트를 보내기 전에 그 클라이언트의 로컬 연산을 먼저 모두 처리하도록 보장할 수 있음 -
이 글의 핵심 메시지는 CRDT/OT의 전체 복잡성이 중앙 서버가 없을 때만 필요하다는 건가?
- 중앙 서버가 없어도, 연산들을 결국 전체 순서로 정렬하고 그 순서대로 적용하는 탈중앙 방법이 있다면 CRDT/OT의 복잡성을 피할 수 있음: https://mattweidner.com/2025/05/21/text-without-crdts.html#d...
다른 댓글들이 말하듯 기술적으로는 이것도 CRDT이고, 꽤 일반적인 형태임. 또한 연산을 되돌리고 재생하는 구현 자체도 만만치 않음. 그래도 각 자료형마다 전통적인 CRDT/OT를 쓰는 것보다는 단순하길 바람 - 그게 CRDT의 핵심임. 같은 자료구조의 여러 복제본이 여러 노드에 걸쳐 관리되고, 각 복제본은 독립적으로 갱신되며, 결국 모두 수렴함
- OT는 중앙 서버가 필요함
- 중앙 서버가 없어도, 연산들을 결국 전체 순서로 정렬하고 그 순서대로 적용하는 탈중앙 방법이 있다면 CRDT/OT의 복잡성을 피할 수 있음: https://mattweidner.com/2025/05/21/text-without-crdts.html#d...
-
이 분야 전문가는 아니지만, Automerge 같은 CRDT와의 주된 차이는 서버 조정으로 보임. 예를 들어 이 글 [1]을 보면 Automerge는 동시 삽입을 처리할 때 순번을 쓰고, 삽입이 동시에 일어나면 합의된 에이전트 ID 순서에 의존함. 반면 이 방식은 서버가 도착 순서대로 처리하는 데 의존함
글에는 “텍스트 편집 CRDT에서는 멋진 알고리즘이 ID 순서를 정해준다. 그 순서 알고리즘이 여러 텍스트 편집 CRDT의 차이를 만들고, CRDT 논문의 복잡한 부분이다. 우리는 그걸 완전히 피한다”는 내용이 있음. 많은 앱에 어차피 중앙 서버가 있으니 “멋진 알고리즘”을 피할 수 있다는 말은 납득됨. 다만 서버 조정에는 로컬 편집의 되돌리기와 재생이 필요해서, 그게 훨씬 단순한지는 100% 확신하기 어려움. [1] https://josephg.com/blog/crdts-go-brrr/- 되돌리기와 재생도 딱히 단순하지는 않다는 데 동의함. 영속 B+Tree도 별로 단순한 물건은 아님
- Automerge는 내부적으로 모든 연산을 결국 일관되는 전체 순서로 저장한다고 알고 있고, 그걸 서버 조정에서 서버의 대체물로 쓸 수 있음: https://mattweidner.com/2025/05/21/text-without-crdts.html#d...
하지만 Automerge는 실제로 그렇게 하지 않고, 텍스트 연산을 전통적인 CRDT인 RGA로 처리함. 아마 지적한 것처럼 연산 되돌리기와 재생 구현이 쉽지 않기 때문일 것임
-
그러니까 최적화 안 된 CRDT인가? 최대 집합 크기를 1로 두고 그냥 밀어붙이는 식?
- 일종의 줄일 수 없는 복잡성처럼 보여서 매력적임. 실제로 일어나는 일에 가깝고 단순함. 말한 대로 최적화는 안 됐겠지만
-
서버 조정을 쓴다는 점 때문에 클라이언트 쪽 조정은 까다로울 것 같음. 서버 업데이트가 도착할 때마다 적용하면서도 부드러운 편집기 UX를 어떻게 유지할까?
예를 들어 클라이언트가 보낸 문자 삽입 요청이 실패하면 그냥 재시도하나? 그 사이 업데이트가 도착했다면? 편집: “Client-Side” 섹션에서 이 경우를 인정하고, 되감기 후 재생을 제안하며, 더 단순한 안으로 대기 큐가 비워질 때까지 막는 방법도 제안함. 프런트엔드 관점에서는 명시되지 않은 UI/UX 예외가 길게 꼬리를 물 수 있어서, 전체적으로는 CRDT가 더 단순할 수도 있어 보임. 그리고 통신이 끊기기 쉬운 뉴욕 지하철에서 편집감은 어떨지도 궁금함- ProseMirror와 최신 CodeMirror는 이 문제에 꽤 우아한 해법이 있음. 문서의 각 변경을 노드/텍스트 식별자가 아니라 인덱스를 추적하는 단계(step) 로 모델링하고, “위치 맵”이라는 자료구조를 써서 버퍼된 단계들을 새 위치로 매핑한 뒤 문서에 적용함
실제로 꽤 잘 동작함. 자세한 내용은 여기 있음:
https://marijnhaverbeke.nl/blog/collaborative-editing.html
https://marijnhaverbeke.nl/blog/collaborative-editing-cm.htm...
- ProseMirror와 최신 CodeMirror는 이 문제에 꽤 우아한 해법이 있음. 문서의 각 변경을 노드/텍스트 식별자가 아니라 인덱스를 추적하는 단계(step) 로 모델링하고, “위치 맵”이라는 자료구조를 써서 버퍼된 단계들을 새 위치로 매핑한 뒤 문서에 적용함