지난 2년간 경매 시스템을 3개 구축했습니다. 첫 번째는 엉망이었습니다. 매초 데이터베이스를 폴링하고, 경쟁 조건이 곳곳에 있었고, 입찰이 사라졌습니다. 두 번째는 더 나았지만 메인 API 옆에 별도의 WebSocket 서버를 관리해야 했습니다. 세 번째는? 그것이 제가 당신에게 보여줄 것입니다. Supabase Realtime을 사용하고, 입찰 엔진 구축이 처음으로 제대로 느껴졌습니다.

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

처음부터 구축해봅시다.

목차

아키텍처 개요

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

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

  • 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개의 쿼리가 됩니다. 대부분은 새로운 것이 없습니다. WebSockets은 이 모델을 뒤집습니다: 아무것도 변하지 않으면 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;
  -- 존재하는 경우 기존 게시를 제거합니다
  DROP PUBLICATION IF EXISTS supabase_realtime;
  
  -- 두 테이블로 게시 생성
  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 잠금은 중요합니다. 없으면 동시에 도착하는 두 개의 입찰이 같은 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 훅

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는 첫 번째 방어선입니다. 하지만 고경합 경매를 위해 사용하기 시작한 다른 패턴이 있습니다. advisory locks입니다:

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;
  
  -- advisory 잠금 획득 (같은 경매에서 동시 입찰 차단)
  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 클라이언트 설정에서 eventsPerSecond: 20
  • 클라이언트 측: 입찰 버튼을 300ms로 디바운스하여 이중 클릭 방지
  • UI 업데이트: 입찰 목록 애니메이션에 requestAnimationFrame 사용

경매 종료 시간

클라이언트 시계를 신뢰하지 마세요. PostgreSQL cron 작업을 pg_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 무제한 (자체 확장)
인증 통합 내장 (RLS + JWT) 커스텀 토큰 기반 Firebase Auth 커스텀
Presence ✅ 내장 ✅ 내장 ✅ 내장 ✅ 내장 ✅ 내장
무료 계층 500K MAU, 200 동시 100 연결 6M msgs/mo 1GB 저장됨 $0 (호스팅 비용)
Pro 가격 $25/mo $49/mo $29/mo 종량제 ~$100-500/mo (AWS)
최적 대상 DB 중심 실시간 앱 간단한 pub/sub 높은 안정성 모바일 앱 완전한 제어

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

헤드리스 CMS 아키텍처를 구축하고 있다면, Supabase는 다른 서비스를 관리할 필요 없이 콘텐츠 전달과 함께 자연스럽게 적합합니다.

경매 시스템 배포 및 확장

대부분의 프로젝트의 경우 Supabase의 관리형 Pro 계층인 월 $25는 일일 10,000개의 경매까지 편안하게 처리합니다. 주의할 점은:

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

Astro 또는 Next.js로 구축한 프론트엔드의 경우, Supabase JS 클라이언트는 동일하게 작동합니다. Realtime 구독을 위해 클라이언트 측에서 초기화하기만 확인하세요.

진지하게 확장을 처리해야 하는 것을 구축하고 있다면 - 수십만의 동시 입찰자 - 당신에게 연락하세요. 우리는 규모에서 이러한 시스템을 설계했으며 함정을 피하도록 도와줄 수 있습니다. 또한 프로젝트 기반 참여를 위해 가격 페이지를 확인할 수 있습니다.

FAQ

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

Supabase Realtime은 라이브 경매에 충분히 빠릅니까? 네. 제 테스트에서 입찰 삽입에서 클라이언트 알림까지의 종단 간 레이턴시는 평균 약 50-80ms이고 p99는 100ms 미만입니다. 참고로 인간의 반응 시간은 약 200-300ms이므로 입찰이 사실상 즉시 나타납니다. 병목 지점은 거의 항상 Supabase가 아닙니다. 보통 클라이언트의 네트워크 연결입니다.

두 사람이 동시에 입찰할 때 경쟁 조건을 어떻게 방지합니까? 트리거 함수 내부에서 PostgreSQL의 SELECT ... FOR UPDATE 행 수준 잠금을 사용하거나 pg_advisory_xact_lock()을 통해 advisory 잠금을 사용합니다. 이는 경매당 입찰 처리를 직렬화하므로 한 번에 하나의 입찰만 검증됩니다. "지는" 입찰은 여전히 검증됩니다. 승자로부터 업데이트된 높은 입찰을 단순히 보고 성공하거나 (여전히 더 높으면) 적절한 오류로 실패합니다.

Supabase Realtime을 Next.js 또는 Astro와 함께 사용할 수 있습니까? 절대적으로. @supabase/supabase-js 클라이언트는 모든 JavaScript 환경에서 작동합니다. Next.js의 경우 Supabase 클라이언트를 클라이언트 컴포넌트에서 초기화하고 (Realtime은 브라우저 WebSocket이 필요하기 때문에) useEffect 훅 내부에서 사용합니다. Astro의 경우 클라이언트 측 대화형 아일랜드에서 사용합니다. 구독 코드는 프레임워크 선택과 상관없이 동일합니다.

경매 중에 사용자의 연결이 끊어지면 어떻게 됩니까? Supabase Realtime은 자동으로 재연결을 시도합니다. 클라이언트가 재연결되고 다시 구독할 때 현재 상태를 받습니다. 중요한 경매의 경우 표준 쿼리를 통해 최신 경매 상태를 재연결할 때 가져오는 것을 권장하여 연결 해제 기간 동안 아무것도 놓치지 않도록 합니다. Presence 시스템은 타임아웃 후 자동으로 연결 해제된 사용자를 제거합니다.

경매 종료 시간을 정확하게 어떻게 처리합니까? 클라이언트 측 타이머를 경매 종료 시간에 신뢰하지 마십시오. 조작될 수 있습니다. pg_cron 확장을 사용하여 PostgreSQL cron 작업을 사용하여 만료된 경매를 10초마다 확인하고 닫습니다. 클라이언트에 서버 타임스탬프를 전송하여 카운트다운을 표시할 수 있지만 실제 종료 결정은 항상 데이터베이스에서 발생합니다.

작은 프로젝트의 경우 Supabase Realtime이 무료입니까? Supabase의 무료 계층은 200개의 동시 연결과 500,000명의 월별 활성 사용자가 있는 Realtime을 포함합니다. 이는 취미 경매 사이트 또는 MVP에 충분합니다. 의미 있는 트래픽이 있는 프로덕션 경매 플랫폼을 운영하고 있다면 월 $25 가격인 Pro 계층인데 초당 $0.09/GB 이그레스이면 시작하는 곳입니다. 자신의 WebSocket 인프라를 실행하는 것보다 훨씬 저렴합니다.

실시간 입찰 시스템을 로컬에서 어떻게 테스트합니까? Supabase CLI (supabase start)를 사용하여 Realtime이 활성화된 로컬 Supabase 인스턴스를 실행합니다. 여러 브라우저 탭을 열어 여러 입찰자를 시뮬레이션합니다. 부하 테스트의 경우 100개 이상의 Supabase 클라이언트를 생성하고 타이머에 따라 경매에 대해 입찰하게 하는 간단한 Node.js 스크립트를 사용합니다. 이는 경쟁 조건을 포착하고 프로덕션으로 가기 전에 eventsPerSecond 파라미터를 튜닝하는 데 도움이 됩니다.