무료 챕터

6. 실시간 데이터 동기화와 협업

by leancle2025. 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와 대용량 데이터 처리 최적화!