이커머스 푸시 알림 전략: 재참여율을 높이는 개인화 푸시 설계

이커머스

푸시 알림재참여 마케팅FCM개인화 마케팅앱 마케팅

이 글은 누구를 위한 것인가

  • 푸시 알림 발송하는데 클릭율이 1% 이하인 팀
  • 모든 고객에게 같은 메시지를 보내다가 수신 거부율이 높아진 팀
  • 장바구니 이탈·가격 하락 자동 알림을 구현하고 싶은 팀

들어가며

무분별한 푸시 알림은 수신 거부로 이어진다. 개인화된 트리거 푸시는 일반 마케팅 푸시보다 클릭율이 5-10배 높다. "당신이 본 상품이 10% 할인됐어요"는 "오늘만 세일!" 보다 훨씬 효과적이다.

이 글은 bluefoxdev.kr의 이커머스 앱 마케팅 가이드 를 참고하여 작성했습니다.


1. 트리거 푸시 유형별 성과

[트리거 푸시 성과 비교]

1. 장바구니 이탈 알림:
   트리거: 장바구니 담고 24시간 미결제
   메시지: "장바구니에 상품이 기다리고 있어요 🛒"
   평균 클릭율: 8-12%
   전환율: 3-5%

2. 가격 하락 알림:
   트리거: 위시리스트/조회 상품 가격 10% 이상 하락
   메시지: "찜한 [상품명]이 18,000원으로 내려갔어요!"
   평균 클릭율: 15-25%
   전환율: 8-12%

3. 재입고 알림:
   트리거: 품절 상품 재입고
   메시지: "[상품명] 재입고! 지금 빠르게 담으세요"
   평균 클릭율: 20-30%
   전환율: 10-15%

4. 개인화 추천:
   트리거: 7일 이상 미방문
   메시지: "[고객명]님 취향 맞춤 신상품 도착"
   평균 클릭율: 4-7%

5. 배송 상태 알림:
   트리거: 배송 상태 변경
   메시지: "주문하신 상품이 배송 출발했어요 🚚"
   평균 클릭율: 30-50% (가장 높음)

2. FCM 푸시 발송 시스템

import firebase_admin
from firebase_admin import messaging
from datetime import datetime, timedelta

firebase_admin.initialize_app()

class PushNotificationService:
    
    async def send_push(
        self,
        user_id: str,
        title: str,
        body: str,
        data: dict,
        image_url: str | None = None,
    ) -> bool:
        """단일 사용자 푸시 발송"""
        
        # 발송 가능 여부 확인
        if not await self.can_send_push(user_id):
            return False
        
        fcm_token = await get_user_fcm_token(user_id)
        if not fcm_token:
            return False
        
        message = messaging.Message(
            notification=messaging.Notification(
                title=title,
                body=body,
                image=image_url,
            ),
            data={k: str(v) for k, v in data.items()},
            token=fcm_token,
            android=messaging.AndroidConfig(
                priority="high",
                notification=messaging.AndroidNotification(
                    channel_id="ecommerce_alerts",
                    icon="ic_notification",
                    color="#FF4B2B",
                ),
            ),
            apns=messaging.APNSConfig(
                payload=messaging.APNSPayload(
                    aps=messaging.Aps(
                        badge=1,
                        sound="default",
                    )
                )
            ),
        )
        
        try:
            response = messaging.send(message)
            await self.log_push_sent(user_id, title, data.get("type"), response)
            return True
        except messaging.UnregisteredError:
            await self.deactivate_fcm_token(user_id, fcm_token)
            return False
    
    async def can_send_push(self, user_id: str) -> bool:
        """발송 가능 여부: 수신 거부, 최대 발송 횟수 확인"""
        
        prefs = await get_user_push_preferences(user_id)
        if not prefs["push_enabled"]:
            return False
        
        # 하루 최대 3개, 마케팅은 최대 1개
        today_count = await get_today_push_count(user_id)
        if today_count >= 3:
            return False
        
        # 조용한 시간대 (22:00 - 08:00)
        hour = datetime.now().hour
        if hour >= 22 or hour < 8:
            return False
        
        return True

async def trigger_cart_abandonment_push(user_id: str):
    """장바구니 이탈 자동 알림"""
    
    cart = await get_user_cart(user_id)
    if not cart or not cart["items"]:
        return
    
    top_item = max(cart["items"], key=lambda x: x["price"])
    
    service = PushNotificationService()
    await service.send_push(
        user_id=user_id,
        title="장바구니에 담아둔 상품이 기다려요",
        body=f"{top_item['name']} 외 {len(cart['items'])-1}개 상품",
        data={
            "type": "cart_abandonment",
            "deep_link": "app://cart",
        },
        image_url=top_item["image_url"],
    )

async def trigger_price_drop_push(product_id: str, old_price: int, new_price: int):
    """가격 하락 알림: 위시리스트 사용자 대상"""
    
    drop_pct = (old_price - new_price) / old_price * 100
    
    if drop_pct < 10:  # 10% 미만 하락은 알림 안함
        return
    
    wishlist_users = await get_users_with_product_in_wishlist(product_id)
    product = await get_product(product_id)
    
    service = PushNotificationService()
    
    for user_id in wishlist_users:
        await service.send_push(
            user_id=user_id,
            title=f"찜한 상품이 {drop_pct:.0f}% 할인됐어요!",
            body=f"{product['name']} → {new_price:,}원",
            data={
                "type": "price_drop",
                "product_id": product_id,
                "deep_link": f"app://product/{product_id}",
            },
            image_url=product["image_url"],
        )

마무리

푸시 알림의 원칙은 "고객이 원할 때, 고객이 원하는 내용을"이다. 배송 알림은 언제나 환영받지만, 마케팅 푸시는 하루 1개 이상 보내면 수신 거부율이 급상승한다. 트리거 기반 개인화 푸시부터 시작하고, 일반 마케팅 푸시는 주 2회 이하로 제한하라.