품절 웨이팅·재입고 알림 시스템: 이탈 방지와 전환율 회복 전략

이커머스

품절 알림재입고 알림웨이팅 시스템이탈 방지푸시 알림

이 글은 누구를 위한 것인가

  • "품절입니다" 페이지에서 사용자가 그냥 떠나는 것을 막고 싶은 팀
  • 재입고 알림으로 구매 전환을 회복하고 싶은 이커머스 PM
  • 알림 시스템을 이메일·푸시·SMS 멀티채널로 구축하려는 개발자

들어가며

품절 상품 페이지는 이탈 페이지가 아니다. 웨이팅 등록을 받고, 재입고 시 즉시 알려주면 열성 고객을 놓치지 않을 수 있다. 실제로 재입고 알림의 구매 전환율은 일반 마케팅 이메일보다 5-10배 높다.

이 글은 bluefoxdev.kr의 이커머스 리텐션 전략 을 참고하여 작성했습니다.


1. 시스템 구조

[재입고 알림 플로우]

품절 상품 페이지
  → 사용자: "재입고 알림 받기" 등록
  → DB: waitlist 테이블에 저장

재고 업데이트 이벤트 (입고, 반품)
  → Inventory Service → 이벤트 발행
  → Alert Service 구독
  → 웨이팅 목록 조회
  → 채널별 알림 발송

발송 우선순위:
  1. 등록 순서 (선착순)
  2. VIP 고객 우선 (멤버십 등급)
  3. 알림 채널: 앱 푸시 → 이메일 → SMS

2. DB 스키마 및 API

CREATE TABLE stock_waitlist (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id),
    product_id UUID NOT NULL,
    variant_id UUID,          -- 특정 옵션 (색상, 사이즈)
    notification_channels TEXT[] DEFAULT ARRAY['email'],  -- 'push', 'email', 'sms'
    created_at TIMESTAMPTZ DEFAULT NOW(),
    notified_at TIMESTAMPTZ,  -- 알림 발송 시간
    purchased_at TIMESTAMPTZ, -- 구매 완료 시간
    status VARCHAR(20) DEFAULT 'waiting',  -- waiting, notified, purchased, cancelled
    UNIQUE(user_id, product_id, variant_id)
);

CREATE INDEX idx_waitlist_product ON stock_waitlist(product_id, variant_id, status);
from fastapi import APIRouter, Depends
from datetime import datetime

router = APIRouter(prefix="/waitlist")

@router.post("/{product_id}")
async def join_waitlist(
    product_id: str,
    variant_id: str | None = None,
    channels: list[str] = ["email"],
    user_id: str = Depends(get_current_user),
):
    """재입고 알림 등록"""
    # 이미 등록됐는지 확인
    existing = await db.fetchone(
        "SELECT id FROM stock_waitlist WHERE user_id=$1 AND product_id=$2 AND variant_id=$3 AND status='waiting'",
        user_id, product_id, variant_id
    )
    if existing:
        return {"status": "already_registered"}
    
    await db.execute(
        "INSERT INTO stock_waitlist (user_id, product_id, variant_id, notification_channels) VALUES ($1,$2,$3,$4)",
        user_id, product_id, variant_id, channels
    )
    
    # 현재 대기 순번 반환
    position = await db.fetchval(
        "SELECT COUNT(*) FROM stock_waitlist WHERE product_id=$1 AND variant_id=$2 AND status='waiting' AND created_at < NOW()",
        product_id, variant_id
    )
    
    return {"status": "registered", "position": position + 1}

async def notify_waitlist(product_id: str, variant_id: str | None, quantity: int):
    """재입고 시 웨이팅 사용자 알림 발송"""
    
    # 등록 순서대로 조회 (수량만큼만)
    waiters = await db.fetch("""
        SELECT w.*, u.email, u.push_token, u.phone
        FROM stock_waitlist w
        JOIN users u ON u.id = w.user_id
        WHERE w.product_id = $1 AND w.variant_id = $2 AND w.status = 'waiting'
        ORDER BY w.created_at
        LIMIT $3
    """, product_id, variant_id, quantity * 3)  # 여유분 * 3 (전환율 30% 가정)
    
    for waiter in waiters:
        channels = waiter["notification_channels"]
        
        if "push" in channels and waiter["push_token"]:
            await send_push(waiter["push_token"], {
                "title": "재입고 알림!",
                "body": "기다리시던 상품이 돌아왔습니다. 지금 바로 구매하세요!",
                "data": {"product_id": product_id, "action": "restock"}
            })
        
        if "email" in channels:
            await send_email(waiter["email"], "restock_alert", {
                "product_id": product_id,
                "purchase_url": f"https://myshop.com/products/{product_id}?ref=restock"
            })
        
        await db.execute(
            "UPDATE stock_waitlist SET status='notified', notified_at=$1 WHERE id=$2",
            datetime.utcnow(), waiter["id"]
        )

3. 수요 예측 연동

def estimate_restock_demand(product_id: str) -> dict:
    """웨이팅 목록으로 재고 수요 예측"""
    
    waitlist_count = db.fetchval(
        "SELECT COUNT(*) FROM stock_waitlist WHERE product_id=$1 AND status='waiting'",
        product_id
    )
    
    # 알림 → 구매 전환율 (과거 데이터 기반)
    historical_cvr = 0.35  # 35% 전환 가정
    
    estimated_demand = int(waitlist_count * historical_cvr * 1.2)  # 20% 여유
    
    return {
        "waitlist_count": waitlist_count,
        "estimated_purchase": estimated_demand,
        "recommended_restock_qty": estimated_demand,
    }

마무리

재입고 알림은 "이미 사고 싶었던 사람"에게 보내는 알림이다. 마케팅 이메일과 달리 사용자가 직접 요청한 알림이기 때문에 오픈율과 전환율이 비교할 수 없이 높다. 웨이팅 목록은 수요 예측 데이터로도 활용할 수 있어 재입고 수량 결정에도 도움이 된다.