첫 번째 입찰 엔진은 매초 데이터베이스를 폴링했습니다. 입찰이 잘못된 순서로 도착했습니다. 일부는 완전히 사라졌습니다. 두 번째 버전에서는 별도의 WebSocket 서버를 추가했습니다. 더 나았지만 이제 두 개의 인프라를 관리해야 했습니다. 세 번째 시도에서는 Supabase Realtime을 사용했고 경쟁 조건이 멈췄습니다. 폴링 루프가 없습니다. 독립 실행형 소켓 서버가 없습니다. PostgreSQL 트리거는 입찰 업데이트가 커밋되는 순간 브로드캐스트하고, 연결된 모든 클라이언트는 50밀리초 내에 동일한 상태를 봅니다. 2년 동안 3개의 경매 시스템을 배포했습니다. Supabase 버전은 출시 후 재구축할 필요가 없었던 유일한 시스템입니다. 이것을 가능하게 한 아키텍처와 거의 그것을 깨뜨린 한 가지 함수를 소개합니다.

Supabase Realtime은 PostgreSQL의 WAL(Write-Ahead Log) 위에 놓여 있으며 Elixir 기반 서버를 사용하여 데이터베이스 변경 사항을 WebSocket을 통해 연결된 클라이언트로 푸시합니다. 경매 시스템의 경우, 데이터베이스에 도달하는 모든 입찰이 즉시 그 경매를 보고 있는 모든 입찰자에게 전파된다는 의미입니다. 폴링이 없습니다. 별도의 pub/sub 인프라가 없습니다. 당신의 데이터베이스 당신의 이벤트 시스템입니다.

처음부터 구축해봅시다.

목차

아키텍처 개요

코드를 작성하기 전에 무엇을 구축하고 있는지, 그리고 여러 부분이 어떻게 맞는지 이해해봅시다.

Supabase Realtime은 경매 요구사항에 완벽하게 매핑되는 3가지 기본 요소를 제공합니다:

  • Postgres Changes: 입찰 및 경매 테이블의 INSERT, UPDATE, DELETE 이벤트를 구독합니다. 누군가 입찰하면 모든 구독자는 밀리초 내에 새 행 데이터를 받습니다.
  • Broadcast: 채널 참여자에게 임시 메시지를 보냅니다. "입찰을 능가했습니다"라는 알림에 완벽합니다. 이는 지속될 필요가 없습니다.
  • Presence: 현재 경매를 보고 있는 사람을 추적합니다. 이를 통해 UI에서 "14명의 입찰자가 보고 있습니다"를 표시하고 유령 세션을 감지할 수 있습니다.

데이터 흐름은 다음과 같습니다:

  1. 입찰자가 프론트엔드를 통해 입찰을 제출합니다.
  2. RPC 호출 또는 직접 삽입이 bids 테이블에 도달합니다.
  3. PostgreSQL 트리거는 입찰 금액을 검증하고 auctions.current_high_bid를 업데이트합니다.
  4. Supabase Realtime은 WAL 변경을 감지하고 해당 경매 채널의 모든 구독자에게 푸시합니다.
  5. 두 번째 트리거는 이전 최고 입찰자에게 입찰을 능가했음을 알리는 Broadcast 이벤트를 발생시킵니다.
  6. 모든 연결된 클라이언트는 실시간으로 UI를 업데이트합니다.

입찰 배치에서 모든 클라이언트의 UI 업데이트까지의 지연 시간은 일반적으로 100ms 미만입니다. Supabase Pro 계층의 프로덕션에서 p99를 약 80-90ms로 측정했습니다.

왜 그냥 폴링을 사용하지 않나요?

일부 분들이 "500ms마다 폴링할 수 없나?"라고 생각하고 있는 것을 알고 있습니다. 할 수 있습니다. 하지만 단일 경매에 200명의 동시 입찰자가 있으면 초당 400개의 요청이 데이터베이스에 도달합니다. 그 중 대부분은 새로운 것을 반환하지 않습니다. 이를 50개의 활성 경매로 곱하면 초당 20,000개의 쿼리에 도달합니다. WebSocket은 이 모델을 뒤집습니다. 아무것도 변경되지 않으면 0개의 쿼리, 무언가 변경되면 즉시 업데이트합니다.

데이터베이스 스키마 및 설정

내가 사용하는 스키마는 다음과 같습니다. 의도적으로 단순합니다. 확장할 수 있지만 핵심 구조는 대부분의 경매 유형을 처리합니다.

-- 경매 테이블
CREATE TABLE auctions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  item_name TEXT NOT NULL,
  description TEXT,
  starting_price DECIMAL(12,2) NOT NULL DEFAULT 0,
  current_high_bid DECIMAL(12,2) DEFAULT 0,
  highest_bidder_id UUID REFERENCES auth.users(id),
  min_increment DECIMAL(12,2) DEFAULT 1.00,
  status TEXT NOT NULL DEFAULT 'active'
    CHECK (status IN ('scheduled', 'active', 'ended', 'sold', 'cancelled')),
  starts_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  ends_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '30 minutes',
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- 입찰 테이블
CREATE TABLE bids (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  auction_id UUID NOT NULL REFERENCES auctions(id) ON DELETE CASCADE,
  user_id UUID NOT NULL REFERENCES auth.users(id),
  amount DECIMAL(12,2) NOT NULL,
  placed_at TIMESTAMPTZ DEFAULT NOW(),
  CONSTRAINT positive_amount CHECK (amount > 0)
);

-- 경매별 빠른 입찰 조회를 위한 인덱스
CREATE INDEX idx_bids_auction_amount ON bids(auction_id, amount DESC);
CREATE INDEX idx_bids_auction_time ON bids(auction_id, placed_at DESC);

-- 중요: Realtime을 위한 replica identity 활성화
ALTER TABLE auctions REPLICA IDENTITY FULL;
ALTER TABLE bids REPLICA IDENTITY FULL;

REPLICA IDENTITY FULL 설정은 필수입니다. 이 없으면 Supabase Realtime은 UPDATE 및 DELETE 이벤트에서 기본 키만 받습니다. 전체 행 데이터를 받지 않습니다. 경매 시스템의 경우 클라이언트가 별도의 쿼리를 하지 않고도 입찰 금액을 업데이트할 수 있도록 전체 페이로드가 필요합니다.

Realtime 복제 활성화

Supabase 대시보드에서 Database → Replication으로 이동하고 auctionsbids 테이블에서 복제를 켭니다. 또는 SQL로 다음을 수행할 수 있습니다:

BEGIN;
  -- 기존 publication이 있으면 제거
  DROP PUBLICATION IF EXISTS supabase_realtime;
  
  -- 두 테이블을 사용하여 publication 생성
  CREATE PUBLICATION supabase_realtime FOR TABLE auctions, bids;
COMMIT;

행 수준 보안

이것을 건너뛰지 마세요. RLS는 서버 측 검증 레이어입니다.

ALTER TABLE auctions ENABLE ROW LEVEL SECURITY;
ALTER TABLE bids ENABLE ROW LEVEL SECURITY;

-- 누구나 활성 경매를 볼 수 있습니다.
CREATE POLICY "Public auction viewing" ON auctions
  FOR SELECT USING (status IN ('active', 'ended', 'sold'));

-- 인증된 사용자는 활성 경매의 모든 입찰을 볼 수 있습니다.
CREATE POLICY "View bids on active auctions" ON bids
  FOR SELECT USING (
    EXISTS (
      SELECT 1 FROM auctions
      WHERE auctions.id = bids.auction_id
      AND auctions.status = 'active'
    )
  );

-- 인증된 사용자만 입찰할 수 있습니다.
CREATE POLICY "Place bids" ON bids
  FOR INSERT WITH CHECK (
    auth.uid() = user_id
    AND EXISTS (
      SELECT 1 FROM auctions
      WHERE auctions.id = auction_id
      AND auctions.status = 'active'
      AND auctions.ends_at > NOW()
    )
  );

입찰 로직을 위한 PostgreSQL 트리거

여기가 진정한 마법이 일어나는 곳입니다. 데이터베이스는 모든 입찰 로직을 서버 측에서 적용합니다. 클라이언트는 속임수를 쓸 수 없습니다.

입찰 검증 및 경매 업데이트 트리거

CREATE OR REPLACE FUNCTION process_new_bid()
RETURNS TRIGGER AS $$
DECLARE
  v_auction auctions%ROWTYPE;
BEGIN
  -- 경쟁 조건을 방지하기 위해 경매 행을 잠급니다.
  SELECT * INTO v_auction
  FROM auctions
  WHERE id = NEW.auction_id
  FOR UPDATE;

  -- 경매가 활성 상태인지 검증합니다.
  IF v_auction.status != 'active' THEN
    RAISE EXCEPTION 'Auction is not active';
  END IF;

  -- 경매가 종료되지 않았는지 검증합니다.
  IF v_auction.ends_at < NOW() THEN
    RAISE EXCEPTION 'Auction has ended';
  END IF;

  -- 입찰 금액이 현재 최고 + 최소 증가분을 초과하는지 검증합니다.
  IF NEW.amount < v_auction.current_high_bid + v_auction.min_increment THEN
    RAISE EXCEPTION 'Bid must be at least % higher than current high bid of %',
      v_auction.min_increment, v_auction.current_high_bid;
  END IF;

  -- 자신의 입찰 초과 방지
  IF v_auction.highest_bidder_id = NEW.user_id THEN
    RAISE EXCEPTION 'You are already the highest bidder';
  END IF;

  -- 새 최고 입찰로 경매 업데이트
  UPDATE auctions
  SET
    current_high_bid = NEW.amount,
    highest_bidder_id = NEW.user_id,
    updated_at = NOW()
  WHERE id = NEW.auction_id;

  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER validate_and_process_bid
  BEFORE INSERT ON bids
  FOR EACH ROW
  EXECUTE FUNCTION process_new_bid();

경매 행의 FOR UPDATE 잠금은 매우 중요합니다. 이것이 없으면 동시에 도착하는 2개의 입찰이 동일한 current_high_bid를 모두 읽고, 둘 다 검증을 통과하고, 둘 다 삽입될 수 있습니다. 잠금은 접근을 직렬화합니다.

입찰 능가 알림 브로드캐스트

이 트리거는 성공적인 입찰 에 발생하고 경매 채널에 임시 알림을 보냅니다:

CREATE OR REPLACE FUNCTION notify_outbid()
RETURNS TRIGGER AS $$
DECLARE
  v_previous_bidder UUID;
BEGIN
  -- 방금 입찰을 능가한 사람을 찾으세요.
  SELECT user_id INTO v_previous_bidder
  FROM bids
  WHERE auction_id = NEW.auction_id
    AND id != NEW.id
  ORDER BY amount DESC
  LIMIT 1;

  -- 이전 입찰자가 있으면 입찰 능가 알림을 브로드캐스트합니다.
  IF v_previous_bidder IS NOT NULL THEN
    PERFORM realtime.send(
      jsonb_build_object(
        'auction_id', NEW.auction_id,
        'new_high', NEW.amount,
        'outbid_user', v_previous_bidder,
        'new_leader', NEW.user_id
      ),
      'outbid',
      'auction:' || NEW.auction_id::text,
      true
    );
  END IF;

  RETURN NULL;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER after_bid_notify
  AFTER INSERT ON bids
  FOR EACH ROW
  EXECUTE FUNCTION notify_outbid();

JavaScript를 사용한 클라이언트 측 구독

이제 프론트엔드를 연결해봅시다. vanilla JavaScript/React 패턴으로 표시하겠습니다. Next.js 또는 다른 프레임워크로 구축하는 경우에도 동일한 접근 방식이 작동합니다.

클라이언트 초기화

import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
  {
    realtime: {
      params: {
        eventsPerSecond: 20 // 경매 트래픽을 위해 제한
      }
    }
  }
);

eventsPerSecond 매개변수는 중요합니다. 초당 수십 개의 입찰이 있는 인기 있는 경매에서 초당 50번 다시 렌더링되고 싶지 않습니다. 초당 20개의 업데이트는 부드러운 UI에 충분합니다.

경매 채널 구독

function subscribeToAuction(auctionId, callbacks) {
  const channel = supabase.channel(`auction:${auctionId}`);

  channel
    // Postgres Changes를 통해 새 입찰 수신
    .on('postgres_changes', {
      event: 'INSERT',
      schema: 'public',
      table: 'bids',
      filter: `auction_id=eq.${auctionId}`
    }, (payload) => {
      callbacks.onNewBid(payload.new);
    })

    // 경매 상태 변경 수신
    .on('postgres_changes', {
      event: 'UPDATE',
      schema: 'public',
      table: 'auctions',
      filter: `id=eq.${auctionId}`
    }, (payload) => {
      callbacks.onAuctionUpdate(payload.new);
    })

    // 입찰 능가 브로드캐스트 알림 수신
    .on('broadcast', { event: 'outbid' }, ({ payload }) => {
      callbacks.onOutbid(payload);
    })

    // Presence를 통해 활성 입찰자 추적
    .on('presence', { event: 'sync' }, () => {
      const state = channel.presenceState();
      const bidderCount = Object.keys(state).length;
      callbacks.onPresenceUpdate(bidderCount, state);
    })

    .subscribe(async (status) => {
      if (status === 'SUBSCRIBED') {
        // 사용자의 presence 추적
        await channel.track({
          user_id: supabase.auth.getUser()?.data?.user?.id,
          status: 'watching',
          joined_at: new Date().toISOString()
        });
      }
    });

  return channel;
}

경매 구독을 위한 React Hook

import { useState, useEffect, useCallback } from 'react';

function useAuction(auctionId) {
  const [auction, setAuction] = useState(null);
  const [bids, setBids] = useState([]);
  const [bidderCount, setBidderCount] = useState(0);
  const [isOutbid, setIsOutbid] = useState(false);

  useEffect(() => {
    // 초기 상태 가져오기
    async function loadAuction() {
      const { data: auctionData } = await supabase
        .from('auctions')
        .select('*')
        .eq('id', auctionId)
        .single();
      setAuction(auctionData);

      const { data: bidData } = await supabase
        .from('bids')
        .select('*')
        .eq('auction_id', auctionId)
        .order('amount', { ascending: false })
        .limit(20);
      setBids(bidData || []);
    }
    loadAuction();

    // 실시간 업데이트 구독
    const channel = subscribeToAuction(auctionId, {
      onNewBid: (bid) => {
        setBids(prev => [bid, ...prev].slice(0, 20));
        setIsOutbid(false);
      },
      onAuctionUpdate: (updated) => setAuction(updated),
      onOutbid: (payload) => {
        const currentUser = supabase.auth.getUser()?.data?.user;
        if (payload.outbid_user === currentUser?.id) {
          setIsOutbid(true);
        }
      },
      onPresenceUpdate: (count) => setBidderCount(count)
    });

    return () => {
      supabase.removeChannel(channel);
    };
  }, [auctionId]);

  const placeBid = useCallback(async (amount) => {
    const user = (await supabase.auth.getUser()).data.user;
    const { data, error } = await supabase
      .from('bids')
      .insert({
        auction_id: auctionId,
        amount: parseFloat(amount),
        user_id: user.id
      })
      .select()
      .single();

    if (error) throw new Error(error.message);
    return data;
  }, [auctionId]);

  return { auction, bids, bidderCount, isOutbid, placeBid };
}

경쟁 조건 및 입찰 검증 처리

경쟁 조건은 경매 시스템에서 가장 큰 버그의 단일 원인입니다. 이는 내가 그들을 처리하는 방법입니다.

서버 측: PostgreSQL이 무거운 들어올리기를 수행합니다.

트리거 함수의 SELECT ... FOR UPDATE는 첫 번째 방어선입니다. 하지만 내가 사용하기 시작한 다른 패턴이 있습니다. 높은 경합 경매를 위한 자문 잠금:

CREATE OR REPLACE FUNCTION place_bid_safe(
  p_auction_id UUID,
  p_user_id UUID,
  p_amount DECIMAL
)
RETURNS TABLE(bid_id UUID, new_high DECIMAL) AS $$
DECLARE
  v_lock_key BIGINT;
  v_bid_id UUID;
BEGIN
  -- 경매 UUID에서 결정론적 잠금 키 생성
  v_lock_key := ('x' || left(p_auction_id::text, 15))::bit(64)::bigint;
  
  -- 자문 잠금 획득 (동일 경매의 동시 입찰 차단)
  PERFORM pg_advisory_xact_lock(v_lock_key);

  -- 이제 삽입이 안전합니다. (트리거가 검증 처리)
  INSERT INTO bids (auction_id, user_id, amount)
  VALUES (p_auction_id, p_user_id, p_amount)
  RETURNING id INTO v_bid_id;

  RETURN QUERY
  SELECT v_bid_id, p_amount;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

Supabase의 RPC를 사용하여 클라이언트에서 이를 호출합니다:

const { data, error } = await supabase.rpc('place_bid_safe', {
  p_auction_id: auctionId,
  p_user_id: user.id,
  p_amount: bidAmount
});

클라이언트 측: 롤백을 통한 낙관적 UI

UI에서 입찰을 즉시 표시하되 서버가 거부하면 롤백할 준비를 합니다:

async function handleBidSubmit(amount) {
  const optimisticBid = {
    id: crypto.randomUUID(),
    amount,
    user_id: user.id,
    placed_at: new Date().toISOString(),
    _optimistic: true
  };

  // 즉시 표시
  setBids(prev => [optimisticBid, ...prev]);

  try {
    await placeBid(amount);
    // 실제 입찰은 Realtime을 통해 도착하여 낙관적 입찰을 대체합니다.
  } catch (err) {
    // 실패 시 낙관적 입찰 제거
    setBids(prev => prev.filter(b => b.id !== optimisticBid.id));
    showError(err.message);
  }
}

활성 입찰자 추적을 위한 Presence

경매를 보고 있는 사람의 수를 표시하면 긴박함이 생깁니다. Presence 추적은 Supabase에서 매우 간단합니다:

// 사용자가 입찰을 시작할 때 상태 업데이트
async function updatePresenceStatus(channel, status) {
  await channel.track({
    user_id: user.id,
    status, // 'watching', 'bidding', 'won'
    last_active: new Date().toISOString()
  });
}

표시 측에서는 presence 상태를 분석하여 활동적으로 입찰 중인 사람과 단순히 보고 있는 사람이 몇 명인지 표시할 수 있습니다:

function parseBidderStats(presenceState) {
  const users = Object.values(presenceState).flat();
  return {
    total: users.length,
    bidding: users.filter(u => u.status === 'bidding').length,
    watching: users.filter(u => u.status === 'watching').length
  };
}

성능 튜닝 및 프로덕션 고려사항

제한 및 디바운싱

경매 전쟁은 초당 수십 개의 이벤트를 생성할 수 있습니다. 여기가 내가 구성하는 것입니다:

  • 서버 측: Supabase 클라이언트 config에서 eventsPerSecond: 20
  • 클라이언트 측: 입찰 버튼을 300ms에서 디바운스하여 이중 클릭 방지
  • UI 업데이트: 입찰 목록 애니메이션을 위해 requestAnimationFrame 사용

경매 종료 시기

클라이언트 시계를 신뢰하지 마세요. 조작할 수 있습니다. pg_cron을 통해 PostgreSQL cron 작업을 사용합니다:

-- 10초마다 실행하여 만료된 경매 종료
SELECT cron.schedule(
  'close-expired-auctions',
  '*/10 * * * * *',
  $$
  UPDATE auctions
  SET status = CASE
    WHEN highest_bidder_id IS NOT NULL THEN 'sold'
    ELSE 'ended'
  END
  WHERE status = 'active'
  AND ends_at <= NOW();
  $$
);

스나이핑 방지 확장

대부분의 경매 플랫폼은 마지막 몇 초 동안 입찰이 들어오면 기한을 연장합니다:

-- process_new_bid 트리거에 추가
IF v_auction.ends_at - NOW() < INTERVAL '30 seconds' THEN
  UPDATE auctions
  SET ends_at = ends_at + INTERVAL '30 seconds'
  WHERE id = NEW.auction_id;
END IF;

Supabase Realtime vs 대안

프로덕션에서 이들 대부분을 사용했습니다. 솔직한 비교입니다:

기능 Supabase Realtime Pusher Ably Firebase RTDB Socket.io (자체 호스팅)
네이티브 DB 동기화 ✅ PostgreSQL WAL ❌ 별도 서비스 ❌ 별도 서비스 ✅ JSON 트리 ❌ 수동
지연 시간 (p99) ~80-100ms ~60ms ~50ms ~100ms ~40ms (인프라에 따라 다름)
최대 이벤트/초 200k+ 10k (Pro) 50k 100k 무제한 (확장)
인증 통합 Built-in (RLS + JWT) 사용자 정의 토큰 기반 Firebase Auth 사용자 정의
Presence ✅ Built-in ✅ Built-in ✅ Built-in ✅ Built-in ✅ Built-in
무료 계층 500K MAU, 200 동시 100 연결 600만 메시지/월 1GB 저장됨 $0 (호스팅 비용)
Pro 가격 $25/월 $49/월 $29/월 종량제 ~$100-500/월 (AWS)
최적 용도 DB 중심 실시간 앱 간단한 pub/sub 높은 신뢰성 모바일 앱 완전한 제어

특히 경매 시스템의 경우 Supabase가 우승합니다. 입찰이 이미 PostgreSQL에 있기 때문입니다. 데이터베이스와 별도의 pub/sub 시스템 간에 동기화할 필요가 없습니다. 입찰은 DB를 사용하고, DB는 WebSocket 푸시를 트리거합니다. 단 하나의 진실 공급원입니다.

경매 시스템 배포 및 확장

대부분의 프로젝트의 경우 Supabase 관리형 Pro 계층은 월 $25에 일일 10,000개의 경매를 편안하게 처리합니다. 다음을 관찰하세요:

  • 연결 제한: Pro 계층은 500개의 동시 Realtime 연결을 제공합니다. 더 많이 필요한 경우 업그레이드하거나 클라이언트에서 연결 풀링을 구현해야 합니다.
  • WAL 크기: 높은 볼륨 입찰은 상당한 WAL 트래픽을 생성합니다. 복제 슬롯을 모니터링하여 디스크 팽창을 피합니다.
  • 채널 수: 각 경매는 고유한 채널을 얻습니다. 수천 개의 활성 경매를 사용하면 클라이언트가 종료된 경매에서 제대로 구독을 취소하는지 테스트합니다.

또한 원한다면 우리에게 연락해야 합니다. 우리는 규모에서 이러한 시스템을 설계했으며 함정을 피하는 데 도움을 드릴 수 있습니다.

FAQ

Supabase Realtime은 몇 개의 동시 입찰자를 처리할 수 있나요?

Supabase Realtime은 관리형 플랫폼의 분산 서버 전체에서 초당 200,000개 이상의 이벤트를 처리할 수 있습니다. Pro 계층은 프로젝트당 최대 500개의 동시 연결을 지원합니다. 더 큰 경매의 경우 Enterprise 계층은 맞춤 제한을 제공하거나 자신의 인프라에서 Realtime 서버 (오픈 소스)를 자체 호스팅할 수 있습니다.

Supabase Realtime은 라이브 경매에 충분히 빠른가요?

예. 내 테스트에서 입찰 삽입에서 클라이언트 알림까지의 종단 간 지연은 평균 약 50-80ms이고 p99는 100ms 미만입니다. 맥락을 위해 인간의 반응 시간은 약 200-300ms입니다. 따라서 입찰이 효과적으로 즉시 나타납니다. 병목은 거의 Supabase입니다. 일반적으로 클라이언트의 네트워크 연결입니다.

두 사람이 동시에 입찰할 때 경쟁 조건을 어떻게 방지하나요?

트리거 함수 내에서 PostgreSQL의 SELECT ... FOR UPDATE 행 수준 잠금을 사용하거나 pg_advisory_xact_lock()을 통해 자문 잠금을 사용합니다. 이는 경매별 입찰 처리를 직렬화하므로 한 번에 하나의 입찰만 검증됩니다. "손실된" 입찰은 여전히 검증됩니다. 승자의 업데이트된 최고 입찰을 봅니다. 그리고 여전히 더 높으면 성공하거나 적절한 오류로 실패합니다.

Next.js 또는 Astro와 함께 Supabase Realtime을 사용할 수 있나요?

절대적으로. @supabase/supabase-js 클라이언트는 모든 JavaScript 환경에서 작동합니다. Next.js의 경우 클라이언트 컴포넌트에서 Supabase 클라이언트를 초기화합니다 (Realtime은 브라우저 WebSocket이 필요하기 때문). useEffect hooks 내에서 이를 사용합니다. Astro의 경우 클라이언트 측 대화형 섬에서 사용합니다. 구독 코드는 프레임워크 선택과 관계없이 동일합니다.

사용자의 연결이 경매 중간에 끊어지면 어떻게 되나요?

Supabase Realtime은 자동으로 재연결을 시도합니다. 클라이언트가 재연결되고 다시 구독하면 현재 상태를 받습니다. 중요한 경매의 경우 재연결할 때 표준 쿼리를 통해 최신 경매 상태도 가져오는 것을 권장합니다. 단절 기간 동안 놓친 것이 없도록 합니다. Presence 시스템은 자동으로 시간 초과 후 연결이 끊긴 사용자를 제거합니다.

경매 종료 시간을 정확하게 처리하려면 어떻게 해야 하나요?

경매 종료 시간을 위해 클라이언트 측 타이머에 의존하지 마세요. 조작할 수 있습니다. PostgreSQL pg_cron 확장을 사용하여 만료된 경매를 확인하고 닫는 서버 측 cron 작업을 실행합니다. 클라이언트에 서버 타임스탠프를 전송하여 카운트다운을 표시할 수 있지만 실제 종료 결정은 항상 데이터베이스에서 발생합니다.

소규모 프로젝트의 경우 Supabase Realtime이 무료인가요?

Supabase의 무료 계층에는 최대 200개의 동시 연결과 500,000개의 월간 활성 사용자가 있는 Realtime이 포함됩니다. 취미 경매 사이트 또는 MVP에 충분합니다. 의미 있는 트래픽이 있는 프로덕션 경매 플랫폼을 실행하는 경우 월 $25에 초당 $0.09/GB 송신이 있는 Pro 계층을 시작해야 합니다. 자신의 WebSocket 인프라를 실행하는 것보다 훨씬 저렴합니다.

실시간 입찰 시스템을 로컬에서 어떻게 테스트하나요?

Supabase CLI (supabase start)를 사용하여 Realtime이 활성화된 로컬 Supabase 인스턴스를 실행합니다. 여러 브라우저 탭을 열어 여러 입찰자를 시뮬레이션합니다. 부하 테스트의 경우 타이머에서 서로에게 입찰하는 100개 이상의 Supabase 클라이언트를 만드는 간단한 Node.js 스크립트를 사용합니다. 이는 경쟁 조건을 발견하고 프로덕션으로 이동하기 전에 eventsPerSecond 매개변수를 조정하는 데 도움이 됩니다.