← 활용 사례

외부 서비스 웹훅을 사용자가 보고 있는 페이지에 보여 주기

결제 확인, 배송 이동, 빌드 완료: Stripe, 택배사, GitHub 웹훅을 사용자가 보고 있는 페이지로 새로고침 없이 전달합니다.

고객이 Stripe Checkout에서 결제하고 '감사합니다' 페이지로 돌아와도, 결제를 확정해 주는 웹훅은 몇 초 늦게 도착할 수 있습니다. 페이지에는 '결제를 확인하는 중…'이 떠 있고, 고객은 새로고침하거나 심지어 한 번 더 결제합니다. 택배사, CI 서비스, 전자서명 도구, 마켓플레이스도 마찬가지입니다. 소식은 서버에 도착할 뿐, 사용자가 보고 있는 페이지에는 오지 않습니다. Hibira를 쓰면 웹훅 핸들러가 받는 즉시 그 업데이트를 페이지로 전달합니다.

사용자가 얻는 것

  • 보고 있던 페이지에 새로고침 없이 '결제 완료'가 표시됨.
  • 배송, 빌드, 서명 진행 상황이 저절로 나타남.
  • 첫 결제가 실패한 줄 알고 다시 결제하는 일이 없어짐.

사용하는 기능

  • 주문한 본인만 참여할 수 있는 비공개 채널(private-order-42). 인가 엔드포인트에서 확인합니다.
  • 제공자의 서명을 검증하고 변경을 저장하는 바로 그 웹훅 핸들러에서 호출하는 trigger.
  • 현재 상태를 한 번 읽어 오는 pusher:subscription_succeeded. 페이지가 열리기 전에 도착한 웹훅도 놓치지 않습니다.
브라우저내 서버StripeHibiraCheckout에서 결제/orders/42로 리디렉션'결제를 확인하는 중…'웹훅checkout.session.completedtrigger("status")private-order-42status: paid영수증 표시
제공자는 서버에 알리고, 서버는 Hibira를 통해 페이지에 알린다

1단계: 저장한 뒤 웹훅 전달하기

서명을 검증하고, 이미 처리한 이벤트는 건너뛰고(제공자가 재시도합니다), 새 상태를 저장한 다음 발행합니다:

app.post('/stripe/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  const event = stripe.webhooks.constructEvent(req.body, req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET);
  if (await db.webhookEvents.seen(event.id)) return res.sendStatus(200);
  if (event.type === 'checkout.session.completed') {
    const orderId = event.data.object.metadata.orderId;
    await db.orders.update(orderId, { status: 'paid' });
    await pusher.trigger(`private-order-${orderId}`, 'status', { status: 'paid' });
  }
  res.sendStatus(200);
});

2단계: 본인만 들을 수 있게 하기

누가 구독할 수 있는지는 채널 인가 엔드포인트가 정합니다:

app.post('/pusher/auth', requireLogin, express.urlencoded({ extended: false }), async (req, res) => {
  const id = req.body.channel_name.match(/^private-order-(.+)$/)?.[1];
  if (!id || !(await db.orders.ownedBy(id, req.user.id))) return res.sendStatus(403);
  res.json(pusher.authorizeChannel(req.body.socket_id, req.body.channel_name));
});

3단계: 페이지에서 듣고, 상태를 한 번 읽기

먼저 구독하고, 구독이 준비되면 현재 상태를 한 번 읽습니다. 그러면 웹훅이 페이지보다 먼저 오든 나중에 오든 상관없습니다:

const channel = pusher.subscribe('private-order-42');
channel.bind('status', ({ status }) => show(status));
channel.bind('pusher:subscription_succeeded', async () => {
  const order = await fetch('/api/orders/42').then((r) => r.json());
  show(order.status);
});

다른 웹훅도 같은 방식으로

팁

  • 제공자에게는 빨리 200으로 응답하세요. 느린 작업은 응답 뒤나 작업 큐에서 처리합니다. 긴 작업의 진행률을 참고하세요.
  • 페이지에 필요한 것(상태와 ID)만 전달하고, 제공자의 원본 페이로드는 전달하지 마세요. 개인정보가 들어 있을 수 있습니다.
  • 사용자가 페이지를 닫았다면 푸시 알림 가이드대로 푸시를 보내세요.
  • 팀 전체가 보는 대시보드라면 주문마다가 아니라 팀마다 채널 하나를 쓰면 됩니다.