Real-time collaboration looks effortless until you pull back the curtain. One person types. Another deletes a line three paragraphs up. A third pastes a snippet from Stack Overflow. Somehow the document settles into a single, coherent state. Building that fluidity from scratch, with no prior experience in WebSockets or distributed state, sounds reckless. It also sounds like the right way to actually learn.
This project starts from zero. No borrowed boilerplate. No polished YouTube walkthroughs where the hard parts are skipped in a thirty-second montage. The goal is a collaborative code editor where multiple users can edit the same file simultaneously, seeing each other’s changes—and each other’s cursors—as they happen. Getting there will require figuring out transport layers, consistency models, and the thorny problem of merging concurrent edits without corrupting the document.
What “Real-Time” Actually Means
Most web applications are comfortable with request-response cycles. You submit a form, the server saves, you refresh the page. Real-time collaboration breaks that contract entirely. Every keystroke is an event that must propagate to every other connected client, usually in milliseconds, and arrive in an order that preserves meaning.
WebSockets are the obvious transport choice here because they maintain a persistent, full-duplex connection between client and server. Unlike HTTP polling, which wastes bandwidth asking “anything new?” every few seconds, a WebSocket stays open. When user A types a semicolon, that character becomes a message that travels through the socket to a central server, then fans out to users B and C. That part is relatively straightforward.
The hard part is what happens when B and C type at the exact same moment. If both changes hit the server nearly simultaneously, which one wins? If you simply broadcast messages in arrival order, you risk dropped characters or jumbled text. Naive last-write-wins strategies fail because they ignore intent. If I type “hello” at the start of line one while you type “world” at the start of line one, the result should not be a collision where one of us is erased. It should be “helloworld” or “worldhello,” deterministically chosen. Achieving that requires a synchronization strategy that understands the structure of the document.
Why Starting From Ground Zero Matters
There are excellent frameworks that hide this complexity. Yjs, Automerge, and Socket.IO can abstract away the pain and produce a working prototype in an afternoon. But using them without understanding the primitives underneath is like flying a plane on autopilot without knowing how to read the instruments. When turbulence hits—and in distributed systems, it always hits—you need to know whether the issue is in your network layer, your conflict resolution, or your data model.
The commitment here is to learn the concepts before relying on the libraries. That means manually reasoning through what happens when:
- A client disconnects mid-keystroke and reconnects ten seconds later
- Two users insert text at the same cursor position concurrently
- One user deletes a block that another user is actively editing
- The server crashes and a new node has to reconstruct the document state from scratch
Operational Transformation (OT) and Conflict-free Replicated Data Types (CRDTs) are the two dominant families of solutions for these problems. Google Docs famously built its early architecture on OT, which requires a central server to transform operations against each other before applying them. CRDTs, by contrast, are designed so that concurrent updates can be merged locally without coordination, making them attractive for peer-to-peer or edge-based setups. Choosing between them—or hybrid approaches—requires understanding their trade-offs in memory use, convergence guarantees, and implementation complexity. Reading about those trade-offs is not enough; the plan is to implement both naive and refined versions to see where they break.
The Rebuilds, Mistakes, and Dead Ends
기대치는 현실적으로 조정됩니다. 아무것도 작동하지 않는 구간이 있을 것입니다. 첫 번째 시도에서는 텍스트 변경을 표현하기 위해 단순한 JSON 패치를 사용할 수도 있지만, JSON에는 "문단의 5번 인덱스"라는 개념이 없다는 사실을 깨닫게 될 것입니다. 그 결과, 동일한 인덱스에 두 개의 삽입 작업이 동시에 발생하면 병합되는 대신 서로를 덮어쓰게 됩니다. 두 번째 시도에서는 커스텀 선형 이력 로그(linear history log)를 구축할 수도 있지만, 문서가 커짐에 따라 해당 로그를 재생하는 것이 Big O의 악몽이 된다는 것을 깨닫게 될 것입니다. 세 번째 시도에서는 로컬에서 WebSockets를 작동시키는 데 성공할 수도 있지만, 패킷 손실과 가변적인 지연 시간이 규칙을 뒤바꿔 놓는 실제 네트워크 환경에서는 무너져 내릴 것입니다.
그 마찰이 바로 핵심입니다. 이미 작동하는 저장소를 복사하는 것은 큐가 왜 특정 순서로 비워지는지, 혹은 서버가 왜 버전 벡터(version vector)를 유지하는지에 대한 탐구 과정을 건너뛰게 만듭니다. 동일한 컴포넌트를 세 번 다시 만드는 것은 느리지만, 프레임워크가 수행하는 일과 사용자의 로직이 처리해야 하는 일 사이의 경계를 이해하도록 강제합니다.
이 과정에 대한 문서는 하이라이트 영상이 아닐 것입니다. 잘못된 방향으로 갔던 기록들도 포함될 것입니다. 예를 들어, 프레젠스 인식(presence awareness)—누가 온라인인지, 커서가 어디에 있는지 아는 것—은 텍스트 자체와 동일한 일관성 모델에 의존한다는 것을 깨닫기 전까지는 단순한 미적 기능처럼 보입니다. 만약 사용자 A가 사용자 B의 커서를 10번째 열에서 보고 있다면, 사용자 B가 네 글자를 삽입했을 때 그 커서는 어디로 이동해야 할까요? 문서 토폴로지(document topology)에 대한 공유된 이해가 없다면, 프레젠스 데이터는 실제와 어긋나게 됩니다. 이를 해결하려면 커서 위치를 단순한 숫자 인덱스가 아닌, 기반 데이터 구조의 식별자(identity)와 결합해야 합니다. 이러한 세부 사항들은 지루하다는 이유로 튜토리얼에서 대충 넘어가는 부분들이지만, 결코 중요하지 않아서가 아닙니다.
다음 단계
당장 계획된 로드맵은 의도적으로 간결합니다. 첫 번째 마일스톤은 다음과 같습니다:
- 문자 이벤트를 에코(echo)하는 로우(raw) WebSocket 서버를 구축하여 지연 시간과 연결 라이프사이클을 직접 체감하기
- 클라이언트에 단순한 문자열 버퍼를 구현하여 동시성 상황에서 왜 단순한 삽입 순서 방식이 실패하는지 이해하기
- 순서가 있는 시퀀스를 위한 처음부터 만드는(from-scratch) CRDT를 구현하여, 비효율적이더라도 교환 법칙(commutative property)이 실제로 작동하는 것을 확인하기
- 실제 코드 에디터 인터페이스(CodeMirror 또는 Monaco 등)와 점진적으로 통합하여, 에디터의 명령형(imperative) API와 작업 이력(operational history)의 함수형(functional) 특성 사이의 불일치를 해결하기
각 단계에는 작성된 근거가 함께 제공될 것입니다. 왜 이 방식이어야 하는가? 어떤 가정이 틀렸는가? 어떤 추상화가 누수(leak)되었는가?
진정한 교훈
WebSockets나 CRDT에 대한 경험 없이 이런 프로젝트를 시작하는 것은 위협적이지만, 전문성이란 종종 더 나은 이름표를 붙인 반복된 혼란일 뿐입니다. 목표는 빠르게 끝내는 것이 아닙니다. 모든 레이어가 희망에 기대어 가져온 것이 아니라, 의도를 가지고 구축되었기에 그 동작을 예측할 수 있는 시스템을 만드는 것입니다.
텍스트 에디터, 디자인 도구, 혹은 게임 상태 동기화 엔진 등 협업 소프트웨어를 만들어 본 적이 있다면, 당신을 당황하게 했던 실패 사례들을 공유해 주세요. 만약 당신도 이 시스템들을 배우는 중이라면, 함께 따라오세요. 코드는 천천히 올라올 것이며, 자주 다시 작성될 것입니다. Day 0가 지금 시작됩니다.
