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

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

Source: https://hibira.io/ko/use-cases/webhooks-to-browser

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

## 사용자가 얻는 것

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

## 사용하는 기능

- 주문한 본인만 참여할 수 있는 **비공개 채널**(`private-order-42`). 인가 엔드포인트에서 확인합니다.
- 제공자의 서명을 검증하고 변경을 저장하는 바로 그 웹훅 핸들러에서 호출하는 **`trigger`**.
- 현재 상태를 한 번 읽어 오는 **`pusher:subscription_succeeded`**. 페이지가 열리기 전에 도착한 웹훅도 놓치지 않습니다.

```text
sequence: 제공자는 서버에 알리고, 서버는 Hibira를 통해 페이지에 알린다
브라우저 | 내 서버 | Stripe | *Hibira
1 > 3: Checkout에서 결제
3 >> 1: /orders/42로 리디렉션 | '결제를 확인하는 중…'
3 > 2: 웹훅 | checkout.session.completed
2 > 4: trigger("status") | private-order-42
4 > 1: status: paid | 영수증 표시
```

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

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

```js
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단계: 본인만 들을 수 있게 하기

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

```js
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단계: 페이지에서 듣고, 상태를 한 번 읽기

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

```js
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);
});
```

## 다른 웹훅도 같은 방식으로

| 출처 | 웹훅 | 열린 페이지에 보이는 것 |
| --- | --- | --- |
| Stripe, PayPal, Adyen | 결제 성공 또는 실패 | 영수증, 또는 다시 시도 버튼 |
| 택배사 | 배송 추적 업데이트 | 주문 페이지의 새 단계 |
| GitHub, GitLab, Vercel | 빌드 또는 배포 완료 | 상태 배지와 미리보기 링크 |
| 전자서명 도구 | 문서 서명 완료 | '양측 서명 완료' |
| Shopify, 마켓플레이스 | 주문 생성 또는 결제 | 판매자 대시보드에 새 행 |

## 팁

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