Yjs와 CRDT의 환상: 우리가 협업 에디터에서 단순함을 선택해야 하는 이유
최근 몇 년간 프론트엔드 생태계에서 ‘협업 에디터’를 구축한다고 하면 십중팔구 CRDT(Conflict-free Replicated Data Type)와 Yjs가 정답처럼 여겨져 왔습니다. 마치 쿠버네티스가 모든 인프라의 정답인 양 유행했던 것처럼 말이죠. 하지만 15년 넘게 엔지니어링을 해오며 내가 배운 가장 확실한 진리는, 우리가 직면한 문제의 99%는 그렇게 복잡한 기술을 필요로 하지 않는다는 것 입니다.
최근 Hacker News를 뜨겁게 달군 Moment.dev의 블로그 글, [Lies I was told about collaborative editing, Part 2: Why we don’t use Yjs]를 읽고 저는 무릎을 쳤습니다. 이 글은 우리가 그동안 맹목적으로 추종해온 CRDT와 Yjs의 실상을 아주 적나라하게 파헤칩니다.
솔직히 말해서, 저는 예전부터 CRDT의 오버엔지니어링에 대해 회의적이었습니다. 오늘 이 포스팅에서는 왜 Yjs가 대부분의 프로덕션 환경에서 ‘잘못된 선택’이 될 수 있는지, 그리고 왜 단순한 중앙 집중식 아키텍처가 여전히 승리하는지 깊게 파보겠습니다.
40줄의 코드로 충분한 것을 왜 복잡하게 만드는가?
흔히들 오프라인 지원, Optimistic updates, 네트워크 단절 복구를 위해 CRDT가 ‘필수적’이라고 말합니다. 원글의 저자인 Alex Clemmer는 이것이 완전한 착각이라고 지적합니다. 완벽한 Masterless P2P 환경이 필요한 게 아니라면, 이 모든 기능은 단 40줄의 코드로 구현 가능합니다.
핵심은 prosemirror-collab 라이브러리가 사용하는 단순하고 지루한(boring) 알고리즘입니다. 동작 방식은 다음과 같습니다.
- 각 문서에는 Source of Truth를 쥐고 있는 단일 Authority(중앙 서버 등)가 존재합니다.
- 클라이언트는 트랜잭션 스텝과 자신이 마지막으로 본 버전(
lastSeenVersion)을 서버에 제출합니다. - 만약 버전이 맞지 않으면, 클라이언트는 서버로부터 최신 변경 사항을 가져와 자신의 로컬 변경 사항을 그 위에 Rebase 한 뒤 다시 제출합니다.
이게 전부입니다. 본질적으로 Git의 Rebase와 완벽하게 동일한 개념입니다. 이 단순한 구조만으로도 60fps의 Latency 목표를 달성하고, 충돌을 서버에서 깔끔하게 제어할 수 있습니다. 굳이 벡터 클럭(Vector clock)이나 메타데이터로 범벅된 CRDT를 쓸 이유가 없습니다.
Yjs의 치명적인 아키텍처 결함: 매 키보드 입력마다 DOM을 날려버린다고?
이 글에서 가장 충격적이었고, 시니어 엔지니어로서 내 등골을 서늘하게 만든 부분은 바로 y-prosemirror의 Issue #113에 대한 폭로입니다.
현재 Yjs의 ProseMirror 바인딩은 사용자가 키보드를 칠 때마다 전체 문서를 파괴하고 처음부터 다시 생성 합니다.
이게 버그냐고요? 놀랍게도 설계(By design)입니다. 6년 전 Yjs 작성자는 이것이 의도된 동작이라고 밝혔습니다. 퍼포먼스는 당연히 박살이 납니다. 모든 NodeView, Decoration, DOM 엘리먼트가 매 렌더링 사이클마다 재생성됩니다. 커서 위치, Undo 히스토리, 플러그인 상태 매핑이 줄줄이 깨집니다.
텍스트 에디터에서 60fps(약 16ms의 렌더링 예산)를 달성하려면 철저하게 Incremental DOM 업데이트를 수행해야 합니다. Yjs의 방식은 텍스트 에디터가 어떻게 동작해야 하는지에 대한 근본적인 이해가 결여되어 있다고밖에 볼 수 없습니다.
스키마, 권한 관리, 그리고 툼스톤(Tombstone)의 저주
단순히 성능 문제만 있는 것이 아닙니다. 엔터프라이즈급 에디터를 만들 때 마주하는 현실적인 문제들에서 Yjs는 끔찍한 개발자 경험(DX)을 선사합니다.
- 문서 스키마 (Document Schemas): 중앙 서버가 없다는 것은 스키마의 권위자도 없다는 뜻입니다. 누군가 구버전 스키마로 잘못된 노드를 생성하면, Yjs는 이를 우아하게 거절하는 대신 노드 전체를 조용히 삭제해버리고 이를 모든 피어에게 전파합니다. 데이터 유실이 소리 없이 일어나는 것입니다.
- 권한 관리 (Permissions): ‘뷰어’, ‘댓글 작성자’, ‘에디터’ 등 세밀한 권한 제어가 필요할 때, Yjs는 ProseMirror의 트랜잭션을 XML 업데이트로 변환해버리기 때문에 서버 단에서 이 변경이 단순 텍스트 수정인지 댓글 추가인지 파악하고 차단하기가 극도로 어렵습니다.
- 툼스톤 메모리 누수: CRDT는 P2P 환경에서 노드 삭제를 동기화하기 위해 삭제된 데이터의 흔적인 툼스톤을 남깁니다. 모든 피어가 동기화되었는지 알 길이 없으므로, 이 툼스톤을 영원히 메모리에 들고 있거나(RAM 낭비), 임의의 시간(예: 30초) 뒤에 Garbage Collection을 해버립니다(오프라인 유저의 데이터 유실 위험). 중앙 DB를 쓴다면 그냥
changes(lastSeenVersion)쿼리 한 방이면 끝날 일입니다.
Hacker News의 반응: OT vs CRDT의 끝없는 논쟁
Hacker News 스레드(id=47359712)를 보면 반응이 아주 뜨겁습니다.
한 유저는 “CRDT는 밈(Meme)이며 진지한 애플리케이션을 위한 것이 아니다. 그냥 평범한 사람들처럼 OT(Operational Transformation)를 써라” 라고 다소 과격하지만 뼈 있는 일침을 날렸습니다.
물론 Yjs 진영의 반론도 있습니다. “전체 문서를 재생성하는 건 Yjs 코어의 문제가 아니라 y-prosemirror 바인딩의 한계일 뿐이다”라는 주장이죠. 기술적으로는 맞는 말입니다. 하지만 프로덕션을 책임지는 엔지니어 입장에서 그게 무슨 소용입니까? 공식 생태계의 핵심 바인딩 라이브러리가 6년 동안 이런 치명적인 구조적 결함을 방치했다면, 그 기술 스택 전체에 대한 신뢰도가 떨어질 수밖에 없습니다.
저자 Alex Clemmer가 직접 등판해 밝힌 바에 따르면, 그들이 선택한 방식은 전통적인 복잡한 OT도 아닙니다. 변환(Transformation) 단계가 생략된 아주 가벼운 형태의 OT, 즉 ‘Marijn Collab’ 방식입니다. 이는 에디터 상태의 인메모리 표현과 네트워크 동기화 표현 간의 임피던스 불일치를 최소화합니다.
결론: 당신에게 정말 마스터리스 P2P가 필요한가?
이 글을 읽고 있는 여러분의 팀이 지금 협업 에디터를 구축하고 있다면, 스스로에게 아주 냉정하게 물어보시길 바랍니다.
“우리의 프로덕트가 로컬 퍼스트(Local-first) 기반의 완벽한 Masterless P2P 환경을 요구하는가?”
만약 대답이 ‘아니오’라면 (그리고 대부분의 SaaS는 권한 관리, 이미지 스토리지, DB 영속성을 위해 어차피 중앙 서버를 둡니다), Yjs나 Automerge 같은 CRDT를 도입하는 것은 엄청난 오버엔지니어링입니다.
단순한 중앙 집중식 Authority 모델을 사용하세요. 트랜잭션을 DB에 순차적으로 저장하고, 클라이언트가 Rebase 하도록 만드세요. 디버깅은 100배 쉬워질 것이고, 서버는 예측 가능하게 동작할 것이며, 사용자는 빠르고 쾌적한 에디팅 경험을 얻게 될 것입니다.
기술의 우수성은 알고리즘의 복잡도에서 오는 것이 아니라, 사용자에게 제공하는 경험의 안정성에서 옵니다. 때로는 40줄짜리 단순한 코드가 최신 논문에서 튀어나온 알고리즘보다 훨씬 강력한 법입니다.
References: