9148c358d0
Topic_Agent/Topic_Blog/Topics/Topics_Biz/Topics_Meeting/Topics_Rag의 마크다운 지식 문서를 Topic_General/Topic_Programming/Topic_Graphic/Topic_Business 4개 카테고리로 재분류. - 중복 제거: frontmatter의 status:duplicate/merged + duplicate_of/redirect_to 필드로 자기 자신을 중복으로 선언한 리다이렉트 stub 1032개 제거, 완전 동일 내용 파일 472개 제거, 동일 파일명·다른 내용 충돌 시 더 큰(완전한) 버전만 유지(162개 제거) — 총 1639개 중복 제거. - 분류: 폴더 단위로 명확한 항목(AI_and_ML/Coding/Architecture 등 → Programming, Comfyui/Visual_Effects → Graphic, Topics_Biz/Topics_Meeting/사업 등 → Business, Poetic_Blog_Writing/창의성/Game_Design 등 → General)은 폴더 우선순위로, 나머지 혼재 폴더(Topic_Agent/Topic_Blog/Topics 루트/Thinking & Reasoning/Other/UI_UX_Assets)는 title/tags 키워드 스코어링으로 파일 단위 분류(불명확한 경우 General로 폴백). 원본 폴더명은 "From_*" 서브폴더로 보존해 추적 가능성 유지. - 최종 배치: Programming 2784 / General 1608 / Graphic 285 / Business 249 = 4926개 문서. - 에이전트 운영 상태(.astra/.agent/.obsidian/sessions/memory/_company/docs/lessons/_shared/src)는 지식 콘텐츠가 아니므로 재분류 대상에서 제외하고 원위치 유지. - Topics/Topic_email(상위 보호 폴더 Topic_email과 파일명 100% 중복) 삭제 — 보호 폴더 자체는 미변경. - 완전히 비게 된 Topic_Agent/Topic_Blog/Topics_Biz/Topics_Rag 폴더 제거.
5.4 KiB
5.4 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| cs-crdt-patterns | CRDT — 자동 conflict 해결 / 협업 | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
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 (가장 인기, 작음)
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)
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)
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)
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 (자동)
// Alice
docA.text.insert(0, 'A');
// Bob (offline)
docB.text.insert(0, 'B');
// 동기화 → 둘 다 'AB' 또는 'BA' (deterministic)
Server provider
// 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)
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 분리.