🔄 Realtime Collaboration Stack

20 min read
nodejs socketio redis postgresql yjs webrtc react typescript docker nginx cloudflare realtime collaboration crdt websocket

Figma, Notion 같은 싀시간 협업 앱 구축을 위한 Ʞ술 슀택 - WebSocket, CRDT, WebRTC로 만드는 동시 펞집 시슀템

🔄 Realtime Collaboration Stack

여러 사용자가 동시에 작업할 수 있는 싀시간 협업 애플늬쌀읎션 구축
"Build the next Figma, Notion, or Google Docs" - 싀시간 동Ʞ화의 몚든 것


🎯 읎 슀택읎 적합한 겜우

✅ 추천하는 겜우

  • 동시 펞집읎 핵심 Ʞ능 (묞서, 디자읞, 윔드)
  • 싀시간 컀서와 presence 표시 필요
  • 였프띌읞 지원곌 충돌 핎결 필수
  • 낮은 지연시간 (< 100ms) 요구사항
  • 협업읎 제품의 핵심 가치

❌ 닀륞 슀택을 고렀핎알 할 겜우

  • 닚순 채팅만 필요 → Firebase Realtime DB
  • 음방향 슀튞늬밍 → WebRTC + Media Server
  • 턎제 협업 → 음반 CRUD + 알늌

🧩 핵심 구성요소

싀시간 동Ʞ화 엔진

// Yjs + Socket.io êž°ë°˜ 협업 엔진 import * as Y from 'yjs'; import { Server } from 'socket.io'; import { bindSocketIO } from 'y-socket.io'; import Redis from 'ioredis'; class CollaborationServer { private docs = new Map<string, Y.Doc>(); private redis = new Redis.Cluster([...]); constructor(io: Server) { io.on('connection', (socket) => { socket.on('join-document', async (docId: string) => { // 묞서 로드 또는 생성 const doc = await this.loadOrCreateDocument(docId); // ë°© ì°žê°€ socket.join(docId); // 현재 상태 동Ʞ화 const state = Y.encodeStateAsUpdate(doc); socket.emit('sync-initial', state); // Presence 업데읎튞 this.updatePresence(docId, socket.id, 'joined'); // Yjs 바읞딩 bindSocketIO(doc, socket, { gcEnabled: true, pingInterval: 30000 }); }); // 컀서 위치 람로드캐슀튞 socket.on('cursor-position', (data) => { socket.to(data.docId).emit('remote-cursor', { userId: socket.userId, position: data.position, selection: data.selection }); }); }); } private async loadOrCreateDocument(docId: string): Promise<Y.Doc> { if (this.docs.has(docId)) { return this.docs.get(docId)!; } const doc = new Y.Doc(); // Redis에서 묞서 상태 로드 const savedState = await this.redis.get(`doc:${docId}`); if (savedState) { Y.applyUpdate(doc, Buffer.from(savedState, 'base64')); } // 변겜사항 자동 저장 doc.on('update', async (update) => { const state = Y.encodeStateAsUpdate(doc); await this.redis.set( `doc:${docId}`, Buffer.from(state).toString('base64'), 'EX', 86400 // 24시간 TTL ); }); this.docs.set(docId, doc); return doc; } } 

CRDT êž°ë°˜ 데읎터 구조

// 협업 가능한 데읎터 타입 정의 interface CollaborativeDocument { // Yjs 타입 맀핑 content: Y.Text; // 텍슀튞 펞집Ʞ shapes: Y.Array<Shape>; // 도형 (Figma-like) comments: Y.Map<Comment>; // 댓Ꞁ metadata: Y.Map<any>; // 메타데읎터 } // React 컎포넌튞에서 사용 function CollaborativeEditor({ documentId }: Props) { const [doc, setDoc] = useState<Y.Doc>(); const [provider, setProvider] = useState<WebsocketProvider>(); const [awareness, setAwareness] = useState<Awareness>(); useEffect(() => { // Yjs 묞서 쎈Ʞ화 const ydoc = new Y.Doc(); const wsProvider = new WebsocketProvider( 'ws://localhost:1234', documentId, ydoc ); // Awareness (presence) 섀정 const awareness = wsProvider.awareness; awareness.setLocalState({ user: currentUser, cursor: null, selection: null }); // 원격 사용자 추적 awareness.on('change', (changes) => { const users = Array.from(awareness.getStates().entries()) .map(([clientId, state]) => ({ clientId, ...state.user, cursor: state.cursor })); setRemoteUsers(users); }); setDoc(ydoc); setProvider(wsProvider); setAwareness(awareness); return () => { wsProvider.destroy(); }; }, [documentId]); return ( <div className="editor-container"> <RemoteCursors users={remoteUsers} /> <Editor doc={doc} awareness={awareness} /> </div> ); } 

WebRTC P2P 레읎얎

// P2P 연결로 서버 부하 감소 class P2PNetwork { private peers = new Map<string, RTCPeerConnection>(); private dataChannels = new Map<string, RTCDataChannel>(); async connectToPeer(peerId: string, offer?: RTCSessionDescriptionInit) { const pc = new RTCPeerConnection({ iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }] }); // 데읎터 채널 생성 if (!offer) { const channel = pc.createDataChannel('yjs', { ordered: true, maxRetransmits: 3 }); this.setupDataChannel(peerId, channel); } pc.ondatachannel = (event) => { this.setupDataChannel(peerId, event.channel); }; // ICE 후볎 교환 pc.onicecandidate = (event) => { if (event.candidate) { this.signaling.send({ type: 'ice-candidate', target: peerId, candidate: event.candidate }); } }; this.peers.set(peerId, pc); // Offer/Answer 교환 if (offer) { await pc.setRemoteDescription(offer); const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); return answer; } else { const offer = await pc.createOffer(); await pc.setLocalDescription(offer); return offer; } } private setupDataChannel(peerId: string, channel: RTCDataChannel) { channel.onopen = () => { console.log(`P2P connection established with ${peerId}`); // Yjs 동Ʞ화 시작 this.syncWithPeer(peerId); }; channel.onmessage = (event) => { // Yjs 업데읎튞 적용 const update = new Uint8Array(event.data); Y.applyUpdate(this.doc, update); }; this.dataChannels.set(peerId, channel); } } 

였프띌읞 지원 & 충돌 핎결

// IndexedDB륌 사용한 로컬 영속성 class OfflineSync { private db: IDBDatabase; private syncQueue: SyncOperation[] = []; async saveLocal(docId: string, update: Uint8Array) { const tx = this.db.transaction(['documents'], 'readwrite'); const store = tx.objectStore('documents'); // 현재 상태 가젞였Ʞ const existing = await store.get(docId); const doc = new Y.Doc(); if (existing) { Y.applyUpdate(doc, existing.state); } // 업데읎튞 적용 Y.applyUpdate(doc, update); // 저장 await store.put({ id: docId, state: Y.encodeStateAsUpdate(doc), lastModified: Date.now(), pendingSync: !navigator.onLine }); } async syncWhenOnline() { if ('serviceWorker' in navigator && 'SyncManager' in window) { const registration = await navigator.serviceWorker.ready; await registration.sync.register('sync-documents'); } } // Service Worker에서 싀행 self.addEventListener('sync', async (event) => { if (event.tag === 'sync-documents') { event.waitUntil(this.syncPendingDocuments()); } }); } 

💰 읞프띌 비용 분석

| 컎포넌튞 | 최소 구성 | 표쀀 구성 | 대규몚 | |----------|-----------|-----------|---------| | 서버 (Node.js) | $40/월(2 vCPU, 4GB) | $160/월(4 vCPU, 16GB) | $640/월(16 vCPU, 64GB) | | Redis Cluster | $50/월 | $200/월 | $800/월 | | PostgreSQL | $25/월 | $100/월 | $400/월 | | 로드밞런서 | $20/월 | $20/월 | $100/월 | | CDN (100GB) | $10/월 | $50/월 | $200/월 | | TURN 서버 | $20/월 | $80/월 | $320/월 | | 쎝계 | ~$165/월 | ~$610/월 | ~$2,460/월 |

동시 접속자 수용량

  • 최소: ~1,000명
  • 표쀀: ~10,000명
  • 대규몚: ~50,000명

🚀 구현 로드맵

Phase 1: Ʞ볞 싀시간 동Ʞ화 (2죌)

# 프로젝튞 섀정 mkdir realtime-collab && cd realtime-collab npm init -y npm install express socket.io yjs y-websocket y-indexeddb npm install -D typescript @types/node # Ʞ볞 서버 구조 src/ ├── server/ │ ├── index.ts # Express + Socket.io │ ├── collaboration.ts # Yjs 서버 │ └── persistence.ts # Redis 저장 ├── client/ │ ├── hooks/ # React 훅 │ ├── components/ # UI 컎포넌튞 │ └── lib/ # Yjs 큎띌읎얞튞 └── shared/ └── types.ts # 공통 타입 

Phase 2: Presence & Awareness (1죌)

// 싀시간 컀서 구현 interface UserPresence { id: string; name: string; color: string; cursor: { x: number; y: number } | null; selection: Range | null; lastSeen: number; } function usePresence(awareness: Awareness) { const [users, setUsers] = useState<Map<number, UserPresence>>(); useEffect(() => { const updateUsers = () => { const states = awareness.getStates(); const userMap = new Map(); states.forEach((state, clientId) => { if (state.user) { userMap.set(clientId, { ...state.user, cursor: state.cursor, selection: state.selection, lastSeen: Date.now() }); } }); setUsers(userMap); }; awareness.on('change', updateUsers); updateUsers(); // 컀서 위치 업데읎튞 const updateCursor = (e: MouseEvent) => { awareness.setLocalStateField('cursor', { x: e.clientX, y: e.clientY }); }; document.addEventListener('mousemove', updateCursor); return () => { document.removeEventListener('mousemove', updateCursor); }; }, [awareness]); return users; } 

Phase 3: 성능 최적화 (2죌)

// 1. 디바욎싱 & 슀로틀링 const debouncedUpdate = useMemo( () => debounce((update: Uint8Array) => { provider.ws.send(update); }, 100), [provider] ); // 2. 선택적 동Ʞ화 class SelectiveSync { private visibleNodes = new Set<string>(); syncVisibleOnly(doc: Y.Doc, viewport: Viewport) { const nodes = this.getNodesInViewport(doc, viewport); // 뷰포튞에 듀얎옚 녾드만 동Ʞ화 nodes.forEach(node => { if (!this.visibleNodes.has(node.id)) { this.subscribeToNode(node); this.visibleNodes.add(node.id); } }); // 뷰포튞 밖 녾드 구독 핎제 this.visibleNodes.forEach(nodeId => { if (!nodes.find(n => n.id === nodeId)) { this.unsubscribeFromNode(nodeId); this.visibleNodes.delete(nodeId); } }); } } // 3. 압축 & 배치 처늬 const compressionMiddleware = (socket: Socket) => { const updateQueue: Uint8Array[] = []; setInterval(() => { if (updateQueue.length > 0) { const compressed = pako.deflate( Y.mergeUpdates(updateQueue) ); socket.emit('batch-update', compressed); updateQueue.length = 0; } }, 50); // 50ms 배치 }; 

Phase 4: P2P & 확장성 (2-3죌)

// Hybrid 아킀텍처: 서버 + P2P class HybridSync { private serverConnection: WebSocket; private p2pNetwork: P2PNetwork; private syncStrategy: 'server' | 'p2p' | 'hybrid' = 'hybrid'; async initialize() { // 서버 연결 this.serverConnection = new WebSocket('wss://server.com'); // P2P 넀튞워크 구축 this.p2pNetwork = new P2PNetwork(); // 룞의 플얎 목록 받Ʞ const peers = await this.getRoomPeers(); // 가까욎 플얎와 P2P 연결 const nearbyPeers = await this.findNearbyPeers(peers); for (const peer of nearbyPeers) { await this.p2pNetwork.connectToPeer(peer.id); } } // 동Ʞ화 전략 선택 sync(update: Uint8Array) { switch (this.syncStrategy) { case 'p2p': // P2P로만 동Ʞ화 (서버 부하 최소) this.p2pNetwork.broadcast(update); break; case 'server': // 서버로만 동Ʞ화 (안정성 최대) this.serverConnection.send(update); break; case 'hybrid': // P2P 우선, 서버는 백업 this.p2pNetwork.broadcast(update); this.debouncedServerSync(update); break; } } } 

🏗 아킀텍처 팹턮

읎벀튞 소싱 + CQRS

// 몚든 변겜사항을 읎벀튞로 저장 interface CollaborationEvent { id: string; type: 'INSERT' | 'DELETE' | 'FORMAT' | 'MOVE'; userId: string; timestamp: number; data: any; position: number; } class EventStore { async appendEvent(docId: string, event: CollaborationEvent) { await this.db.query( `INSERT INTO events (doc_id, event_id, type, user_id, data, timestamp) VALUES ($1, $2, $3, $4, $5, $6)`, [docId, event.id, event.type, event.userId, event.data, event.timestamp] ); // 읜Ʞ 몚덞 업데읎튞 (비동Ʞ) this.updateReadModel(docId, event); } async getHistory(docId: string, from?: Date, to?: Date) { const events = await this.db.query( `SELECT * FROM events WHERE doc_id = $1 AND timestamp BETWEEN $2 AND $3 ORDER BY timestamp`, [docId, from, to] ); return events.rows; } } 

확장 가능한 아킀텍처

 ┌─────────────┐ │ Load Balancer│ └──────┬──────┘ │ ┌──────────────┮──────────────┐ │ │ ┌──────▌──────┐ ┌──────▌──────┐ │ Node.js │ │ Node.js │ │ Socket.io │◄────────────►│ Socket.io │ └──────┬──────┘ Redis └──────┬──────┘ │ Pub/Sub │ └──────────┬──────────────────┘ │ ┌──────▌──────┐ │Redis Cluster│ │ (Session) │ └──────┬──────┘ │ ┌──────▌──────┐ │ PostgreSQL │ │(Persistence)│ └─────────────┘ 

🔄 Ʞ술 대안

싀시간 동Ʞ화 띌읎람러늬

| 띌읎람러늬 | 장점 | 닚점 | 적합한 겜우 | |-----------|------|------|------------| | Yjs | CRDT êž°ë°˜, 확장 가능 | 학습 곡선 | 복잡한 협업 | | ShareJS | 간닚한 API | 유지볎수 쀑닚 | 레거시 | | OT.js | OT 알고늬슘 | 복잡한 구현 | 텍슀튞 펞집Ʞ | | Automerge | 강력한 CRDT | 묎거움 | 였프띌읞 우선 |

백엔드 슀택 옵션

# Node.js 슀택 (추천) backend: language: TypeScript framework: Express + Socket.io crdt: Yjs persistence: Redis + PostgreSQL # Elixir 슀택 (고성능) backend: language: Elixir framework: Phoenix channels: Phoenix Channels persistence: PostgreSQL + Mnesia # Go 슀택 (확장성) backend: language: Go framework: Gin + Melody crdt: Custom implementation persistence: Redis + CockroachDB 

🏆 성공 사례

Figma

  • Ʞ술: C++ (렌더링) + WebAssembly + CRDTs
  • 특징: 람띌우저에서 싀행되는 디자읞 도구
  • 성곌: 싀시간 협업의 표쀀

Notion

  • Ʞ술: React + WebSocket + 자첎 동Ʞ화 엔진
  • 특징: 랔록 êž°ë°˜ 펞집Ʞ
  • 규몚: 수백만 동시 사용자

Miro

  • Ʞ술: Canvas API + WebSocket + Redis
  • 특징: 묎한 캔버슀 화읎튞볎드
  • 성능: 100명+ 동시 협업

📚 필수 학습 자료

CRDT 읎론

싀시간 시슀템

사례 연구


💬 싀묎자 ì¡°ì–ž

"CRDT vs OT 선택읎 쀑요핎요. 텍슀튞는 OT가 간닚하지만, 복잡한 데읎터는 CRDT가 답입니닀." - @realtime_expert

"처음부터 P2P 하지 마섞요. 서버 Ʞ반윌로 안정화시킚 닀음 P2P로 최적화하는 게 맞습니닀." - @collaboration_dev

"였프띌읞 지원은 처음부터 섀계하섞요. 나쀑에 추가하멎 아킀텍처 닀시 짜알 핎요." - @offline_first

"성능볎닀 쀑요한 걎 음ꎀ성입니닀. 데읎터 손싀읎나 충돌은 사용자 신뢰륌 잃는 지늄Ꞟ읎에요." - @data_integrity_advocate


⚡ 성능 최적화 팁

렌더링 최적화

// React 18 Concurrent Features 활용 function CollaborativeCanvas() { const [isPending, startTransition] = useTransition(); const handleRemoteUpdate = useCallback((update) => { // 낮은 우선순위로 렌더링 startTransition(() => { applyUpdate(update); }); }, []); // Virtual Scrolling for 대용량 묞서 return ( <VirtualList height={800} itemCount={items.length} itemSize={50} overscan={5} > {({ index, style }) => ( <div style={style}> <CollaborativeItem item={items[index]} /> </div> )} </VirtualList> ); } 

넀튞워크 최적화

// 적응형 동Ʞ화 죌Ʞ class AdaptiveSync { private latency = 0; private syncInterval = 100; adjustSyncRate() { if (this.latency < 50) { this.syncInterval = 50; // 빠륞 넀튞워크 } else if (this.latency < 150) { this.syncInterval = 100; // 볎통 } else { this.syncInterval = 200; // 느며 넀튞워크 } } } 

🚚 죌의사항

볎안

  • WebSocket에 읞슝 믞듀웚얎 필수
  • XSS 방지륌 위한 입력 검슝
  • Rate limiting윌로 DoS ë°©ì–Ž
  • E2E 암혞화 ê³ ë € (믌감한 데읎터)

확장성

  • Sticky Session 또는 Redis Adapter
  • 수평 확장을 위한 상태 분늬
  • 마읎크로서비슀로 Ʞ능 분늬
  • 지역별 엣지 서버 배포

마지막 업데읎튞: 2025-01-28
Ʞ여하Ʞ: GitHub에서 펞집

Found this helpful? Share it with others!
Tweet

🔗 Related Content

You might also be interested in these articles

🏢 company

🏢 Notion Ʞ술 슀택 분석

녞션읎 수천만 사용자에게 All-in-One 워크슀페읎슀륌 제공하는 Ʞ술 슀택 심잵 분석 - 랔록 êž°ë°˜ 에디터, 싀시간 협업, 귞늬고 확장 가능한 데읎터베읎슀

26 min read
notion, typescript+11
Read more
🏢 company

🏢 Figma Ʞ술 슀택 분석

플귞마가 람띌우저에서 싀시간 협업 디자읞 툎을 구현한 Ʞ술 슀택 심잵 분석 - C++, WebAssembly, WebGL로 만든 찚섞대 디자읞 플랫폌

24 min read
figma, c+++11
Read more
🏢 company

🏢 Canva Ʞ술 슀택 분석

캔바가 1억 명 읎상의 사용자에게 쉜고 강력한 디자읞 도구륌 제공하는 Ʞ술 슀택 심잵 분석 - Java, TypeScript, AWS로 구축한 대규몚 디자읞 플랫폌

35 min read
canva, java+10
Read more
🏗 stack

🀖 AI-Powered App Stack

프로덕션 레벚 AI 애플늬쌀읎션 구축을 위한 검슝된 Ʞ술 슀택 - LangChain, FastAPI, Vector DB로 RAG 시슀템 구현

11 min read
python, fastapi+13
Read more
🏢 company

🏢 Discord Ʞ술 슀택 분석

디슀윔드가 전 섞계 1.5억 명에게 싀시간 음성/비디였 통화와 채팅을 제공하는 Ʞ술 슀택 심잵 분석 - Elixir, Rust, WebRTC로 구축한 찚섞대 컀뮀니쌀읎션 플랫폌

26 min read
discord, elixir+12
Read more

Found this helpful?

Help us improve this content by contributing on GitHub or sharing your feedback with the community.