[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
---
|
||||
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]]
|
||||
Reference in New Issue
Block a user