--- id: cs-crdt-patterns title: CRDT — 자동 conflict 해결 / 협업 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [cs, crdt, collab, vibe-coding] tech_stack: { language: "TS / yjs / automerge", applicable_to: ["Frontend", "Backend"] } applied_in: [] aliases: [CRDT, Yjs, Automerge, conflict-free, eventually consistent, OT vs CRDT] --- # CRDT (Conflict-free Replicated Data Type) > 여러 replica 가 다른 변경 후 자동 merge — conflict 없이. **실시간 협업 (Notion, Figma, Google Docs), local-first, offline sync**. **Yjs / Automerge**. ## 📖 핵심 개념 - 모든 operation 가 commutative + idempotent. - Merge 결과 = operation 순서 무관. - State-based vs Operation-based. - OT (Operational Transform) vs CRDT — CRDT 가 modern. ## 💻 코드 패턴 ### Yjs (가장 인기, 작음) ```ts import * as Y from 'yjs'; import { WebsocketProvider } from 'y-websocket'; import { IndexeddbPersistence } from 'y-indexeddb'; const ydoc = new Y.Doc(); // 다른 client 와 동기화 const wsProvider = new WebsocketProvider('ws://localhost:1234', 'doc-1', ydoc); // 로컬 영속 const persistence = new IndexeddbPersistence('doc-1', ydoc); // Y.Map (object) const ymap = ydoc.getMap('config'); ymap.set('theme', 'dark'); ymap.observe((event) => console.log('changed:', event.changes)); // Y.Array const yarray = ydoc.getArray('items'); yarray.push(['item1']); yarray.insert(0, ['first']); // Y.Text (rich text) const ytext = ydoc.getText('doc'); ytext.insert(0, 'Hello'); ``` ### Tiptap + Yjs (collab editor) ```tsx import Collaboration from '@tiptap/extension-collaboration'; import CollaborationCursor from '@tiptap/extension-collaboration-cursor'; const editor = useEditor({ extensions: [ StarterKit.configure({ history: false }), // Yjs 가 history 관리 Collaboration.configure({ document: ydoc }), CollaborationCursor.configure({ provider: wsProvider, user: { name: 'Alice', color: '#f0f' }, }), ], }); ``` ### Awareness (presence) ```ts const awareness = wsProvider.awareness; awareness.setLocalStateField('user', { name: 'Alice', cursor: { x: 100, y: 200 } }); awareness.on('change', () => { const states = Array.from(awareness.getStates().values()); // 다른 사용자 보임 }); ``` ### Automerge (다른 popular CRDT) ```ts import * as A from '@automerge/automerge'; let doc = A.from({ items: [], title: 'Untitled' }); doc = A.change(doc, 'add item', d => { d.items.push({ id: '1', text: 'first' }); }); // Sync (binary) const change = A.getLastLocalChange(doc); sendToPeer(change); // 다른 peer let doc2 = A.from({ items: [], title: 'Untitled' }); doc2 = A.applyChanges(doc2, [change])[0]; ``` ### Merge (자동) ```ts // Alice docA.text.insert(0, 'A'); // Bob (offline) docB.text.insert(0, 'B'); // 동기화 → 둘 다 'AB' 또는 'BA' (deterministic) ``` ### Server provider ```ts // Hocuspocus (Yjs 전용 server) import { Server } from '@hocuspocus/server'; const server = Server.configure({ port: 1234, async onAuthenticate({ token }) { const user = verifyJwt(token); return { user }; }, async onLoadDocument({ documentName }) { const ydoc = new Y.Doc(); const persisted = await db.docs.get(documentName); if (persisted) Y.applyUpdate(ydoc, persisted); return ydoc; }, async onStoreDocument({ documentName, document }) { await db.docs.upsert(documentName, Y.encodeStateAsUpdate(document)); }, }); server.listen(); ``` ### LiveBlocks (managed) ```tsx import { useStorage, useMutation } from '@/liveblocks.config'; const items = useStorage(root => root.items); const addItem = useMutation(({ storage }, text: string) => { storage.get('items').push({ id: nanoid(), text }); }, []); ``` → Yjs / 자체 server 관리 안 해도 됨. ### Local-first ``` 1. 로컬 변경 → IndexedDB 저장 2. 네트워크 OK → server 와 sync 3. Offline → 로컬만 — 정상 동작 4. Reconnect → 자동 merge ``` → Notion / Linear 의 UX. ### 단점 ``` - Document size 시간 따라 커짐 (history 누적). - Garbage collection 필요. - Schema 변경 어려움. - Server-side validation 어려움 (CRDT 가 자유 변경). ``` ### 사용 예 - 협업 editor: Yjs + Tiptap / Lexical. - Real-time dashboard: Yjs + React. - Offline-first app: Automerge / Yjs IndexedDB. - Multi-cursor: Awareness. ## 🤔 의사결정 기준 | 상황 | 추천 | |---|---| | Real-time editor | Yjs + ProseMirror/Tiptap | | 작은 data + JSON-like | Automerge | | Managed | LiveBlocks / Liveblocks | | 단순 sync (last-write-wins) | 직접 구현 | | Strong consistency | DB transaction (CRDT X) | | 1 사용자 / 1 device | CRDT 불필요 | ## ❌ 안티패턴 - **Server-side validation 강 가정**: CRDT 가 client 자유. 별도 룰 + reject. - **Offline 무한 길이**: history 누적. GC. - **CRDT 안에 secret / PII**: client 가 모든 history 접근. - **Schema 자주 변경**: 마이그레이션 어려움. - **너무 큰 document (10MB)**: sync 느림. - **Custom CRDT 자체 구현**: 어려움. Yjs / Automerge 사용. - **LWW (last write wins) 만 사용 + 잃음**: CRDT = 양쪽 보존. ## 🤖 LLM 활용 힌트 - Yjs 가 가장 인기 + 강력. - Hocuspocus 또는 LiveBlocks 가 server 답. - Awareness 로 cursors / presence. - Strong validation 필요 시 server-side check 분리. ## 🔗 관련 문서 - [[React_Editor_Slate_Lexical]] - [[Backend_WebSocket_Scaling]] - [[DB_Distributed_Locks]]