무료 챕터
6. 실시간 데이터 동기화와 협업
by leancle•2025. 8. 9.
⚡ 실시간 마법: 데이터가 살아 움직인다
Supabase의 Realtime 기능과 MCP를 결합하면, Claude가 실시간으로 데이터 변화를 감지하고 반응하는 앱을 만들 수 있습니다.
🔄 Realtime 설정
테이블 Realtime 활성화
You: "orders 테이블에 실시간 기능을 활성화해줘"
Claude: orders 테이블의 Realtime을 활성화하겠습니다.
[MCP 실행 중...]
```sql
-- Realtime 활성화
ALTER PUBLICATION supabase_realtime ADD TABLE orders;
-- 변경사항 브로드캐스트 설정
CREATE OR REPLACE FUNCTION broadcast_order_changes()
RETURNS trigger AS $$
BEGIN
PERFORM pg_notify(
'order_changes',
json_build_object(
'operation', TG_OP,
'record', row_to_json(NEW),
'old_record', row_to_json(OLD)
)::text
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER order_changes_trigger
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH ROW EXECUTE FUNCTION broadcast_order_changes();
✅ Realtime이 활성화되었습니다!
## 📱 실시간 대시보드 구현
### React 컴포넌트 생성
You: "실시간으로 주문 상태를 보여주는 대시보드를 만들어줘"
Claude: 실시간 주문 대시보드를 생성하겠습니다.
components/RealtimeDashboard.tsx:
'use client';
import { useEffect, useState } from 'react';
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
interface Order {
id: string;
user_id: string;
total_amount: number;
status: 'pending' | 'processing' | 'completed' | 'cancelled';
created_at: string;
}
export function RealtimeDashboard() {
const [orders, setOrders] = useState<Order[]>([]);
const [newOrderAlert, setNewOrderAlert] = useState(false);
useEffect(() => {
// 초기 데이터 로드
fetchOrders();
// Realtime 구독
const channel = supabase
.channel('orders-changes')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'orders'
},
(payload) => {
console.log('Change received!', payload);
handleRealtimeUpdate(payload);
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, []);
const fetchOrders = async () => {
const { data, error } = await supabase
.from('orders')
.select('*')
.order('created_at', { ascending: false })
.limit(20);
if (data) setOrders(data);
};
const handleRealtimeUpdate = (payload: any) => {
const { eventType, new: newRecord, old: oldRecord } = payload;
switch (eventType) {
case 'INSERT':
setOrders(prev => [newRecord, ...prev]);
setNewOrderAlert(true);
setTimeout(() => setNewOrderAlert(false), 3000);
break;
case 'UPDATE':
setOrders(prev =>
prev.map(order =>
order.id === newRecord.id ? newRecord : order
)
);
break;
case 'DELETE':
setOrders(prev =>
prev.filter(order => order.id !== oldRecord.id)
);
break;
}
};
const getStatusColor = (status: string) => {
const colors = {
pending: 'bg-yellow-100 text-yellow-800',
processing: 'bg-blue-100 text-blue-800',
completed: 'bg-green-100 text-green-800',
cancelled: 'bg-red-100 text-red-800'
};
return colors[status] || 'bg-gray-100';
};
return (
<div className="p-6">
{newOrderAlert && (
<div className="fixed top-4 right-4 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg animate-pulse">
🎉 새 주문이 들어왔습니다!
</div>
)}
<h2 className="text-2xl font-bold mb-4">
실시간 주문 현황 ({orders.length})
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{orders.map((order) => (
<div
key={order.id}
className="border rounded-lg p-4 shadow hover:shadow-lg transition-shadow"
>
<div className="flex justify-between items-start mb-2">
<span className="text-sm text-gray-500">
#{order.id.slice(0, 8)}
</span>
<span className={`px-2 py-1 rounded text-xs ${getStatusColor(order.status)}`}>
{order.status}
</span>
</div>
<div className="text-lg font-semibold">
₩{order.total_amount.toLocaleString()}
</div>
<div className="text-sm text-gray-600 mt-2">
{new Date(order.created_at).toLocaleString()}
</div>
</div>
))}
</div>
</div>
);
}
🤝 협업 기능 구현
실시간 커서 공유
You: "여러 사용자가 동시에 편집할 때 커서 위치를 공유하는 기능을 만들어줘"
Claude: 실시간 협업 커서를 구현하겠습니다.
**hooks/useCollaborativeCursor.ts:**
```typescript
import { useEffect, useState } from 'react';
import { createClient } from '@supabase/supabase-js';
interface Cursor {
userId: string;
userName: string;
x: number;
y: number;
color: string;
}
export function useCollaborativeCursor(documentId: string) {
const [cursors, setCursors] = useState<Map<string, Cursor>>(new Map());
const [myUserId] = useState(() => crypto.randomUUID());
useEffect(() => {
const channel = supabase.channel(`doc-${documentId}`)
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState();
const newCursors = new Map();
Object.keys(state).forEach(userId => {
if (userId !== myUserId) {
const userState = state[userId][0];
newCursors.set(userId, userState);
}
});
setCursors(newCursors);
})
.on('presence', { event: 'leave' }, ({ key }) => {
setCursors(prev => {
const next = new Map(prev);
next.delete(key);
return next;
});
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({
userId: myUserId,
userName: 'Current User',
x: 0,
y: 0,
color: getRandomColor()
});
}
});
// 마우스 움직임 추적
const handleMouseMove = async (e: MouseEvent) => {
await channel.track({
userId: myUserId,
userName: 'Current User',
x: e.clientX,
y: e.clientY,
color: getRandomColor()
});
};
document.addEventListener('mousemove', handleMouseMove);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
channel.unsubscribe();
};
}, [documentId, myUserId]);
return cursors;
}
📊 실시간 분석 대시보드
라이브 차트 구현
You: "실시간으로 업데이트되는 매출 차트를 만들어줘"
Claude: Chart.js를 사용한 실시간 매출 차트를 구현하겠습니다.
**components/RealtimeSalesChart.tsx:**
```tsx
'use client';
import { useEffect, useRef, useState } from 'react';
import { Chart, registerables } from 'chart.js';
import { createClient } from '@supabase/supabase-js';
Chart.register(...registerables);
export function RealtimeSalesChart() {
const chartRef = useRef<HTMLCanvasElement>(null);
const chartInstance = useRef<Chart | null>(null);
const [salesData, setSalesData] = useState<number[]>([]);
const [labels, setLabels] = useState<string[]>([]);
useEffect(() => {
if (!chartRef.current) return;
// 차트 초기화
chartInstance.current = new Chart(chartRef.current, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: '실시간 매출',
data: salesData,
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.4
}]
},
options: {
responsive: true,
animation: {
duration: 750
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: (value) => `₩${value.toLocaleString()}`
}
}
}
}
});
// Realtime 구독
const channel = supabase
.channel('sales-updates')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'orders'
},
(payload) => {
updateChart(payload.new);
}
)
.subscribe();
// 초기 데이터 로드
loadInitialData();
return () => {
chartInstance.current?.destroy();
supabase.removeChannel(channel);
};
}, []);
const loadInitialData = async () => {
const { data } = await supabase
.from('orders')
.select('created_at, total_amount')
.order('created_at', { ascending: false })
.limit(10);
if (data) {
const reversed = data.reverse();
setLabels(reversed.map(d =>
new Date(d.created_at).toLocaleTimeString()
));
setSalesData(reversed.map(d => d.total_amount));
}
};
const updateChart = (newOrder: any) => {
const time = new Date(newOrder.created_at).toLocaleTimeString();
setLabels(prev => [...prev.slice(-9), time]);
setSalesData(prev => [...prev.slice(-9), newOrder.total_amount]);
if (chartInstance.current) {
chartInstance.current.update();
}
};
return (
<div className="p-6 bg-white rounded-lg shadow">
<h3 className="text-lg font-semibold mb-4">실시간 매출 현황</h3>
<canvas ref={chartRef}></canvas>
</div>
);
}
🎬 다음 챕터 예고
"보안과 성능 최적화" - MCP 환경에서의 보안 best practice와 대용량 데이터 처리 최적화!