우리가 SendRec에 Generic Webhooks를 추가한 방법
Source: Dev.to
Webhook 개요
SendRec은 이미 Slack 알림을 지원하지만, Slack은 하나의 채널에 불과합니다.
누군가 비디오를 시청할 때 n8n 워크플로를 트리거하거나, 시청자가 콜‑투‑액션을 클릭했을 때 CRM에 POST를 보내거나, 모든 이벤트를 맞춤 대시보드로 파이프하고 싶다면 일반 웹훅이 필요합니다 – 모든 이벤트를 JSON POST 형태로 받는 단일 URL입니다.
사용자 설정
- 모든 사용자는 설정에서 웹훅 URL을 구성할 수 있습니다.
- 비디오에 무언가가 발생하면 SendRec이 해당 URL로 JSON 페이로드를
POST합니다.
{
"event": "video.viewed",
"timestamp": "2026-02-21T14:30:00Z",
"data": {
"videoId": "abc-123",
"title": "Product Demo",
"watchUrl": "https://app.sendrec.eu/watch/xyz",
"viewCount": 5,
"viewerHash": "sha256..."
}
}
이벤트 유형
일곱 가지 이벤트 유형이 비디오 전체 수명 주기를 포괄합니다:
| 이벤트 | 설명 |
|---|---|
video.created | 새 비디오 레코드가 생성되었습니다 |
video.ready | 비디오 처리 완료 및 시청 가능 상태가 되었습니다 |
video.deleted | 비디오가 삭제되었습니다 |
video.viewed | 시청자가 비디오를 시청했습니다 |
video.comment | 댓글이 추가되었습니다 |
video.milestone | 조회수 마일스톤에 도달했습니다 |
video.cta_click | 콜투액션 버튼이 클릭되었습니다 |
페이로드 서명
각 요청에는 X-Webhook-Signature 헤더가 포함되어 있어 수신자가 페이로드가 변조되지 않았는지 확인할 수 있습니다.
우리는 HMAC‑SHA256을 사용하며 GitHub 웹훅과 동일한 규칙(sha256= 접두사)을 따릅니다.
func SignPayload(secret string, payload []byte) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
return "sha256=" + hex.EncodeToString(mac.Sum(nil))
}
- 비밀키는 웹훅 URL을 처음 저장할 때 자동으로 생성됩니다 – 32바이트의 무작위 바이트를 16진수로 인코딩한 값입니다.
- 수신자는 요청 본문에 대해 HMAC을 다시 계산하고 이를 헤더 값과 비교합니다.
sha256=접두사는 알고리즘을 명시적으로 표시하며, 기존 통합을 깨뜨리지 않고 향후 알고리즘을 도입할 여지를 남깁니다.
Retry & Delivery Logic
Webhook 엔드포인트가 다운되거나, 배포가 재시작되거나, 로드 밸런서가 일시적으로 멈출 수 있습니다. 단일 실패 전달이 잃어버린 이벤트를 의미해서는 안 됩니다.
Retry Policy
- 지수 백오프(
1초, 그 다음4초)를 적용한 최대 3회 시도. - 지연 시간은 설정 가능(
c.retryDelays). - 컨텍스트 취소가 대기를 중단하므로, 종료 시에도 블로킹되지 않습니다.
func (c *Client) Dispatch(ctx context.Context, userID, webhookURL, secret string, event Event) error {
body, err := json.Marshal(event)
if err != nil {
return fmt.Errorf("marshal webhook payload: %w", err)
}
signature := SignPayload(secret, body)
maxAttempts := 1 + len(c.retryDelays)
var lastErr error
for attempt := 1; attempt = 200 && *statusCode maxResponseBodyBytes {
respBody = respBody[:maxResponseBodyBytes]
}
}
우리는 제한보다 한 바이트 더 읽어 잘림을 감지한 뒤, 정확히 1024 바이트가 되도록 잘라냅니다.
Dispatch Helper (Fire‑and‑Forget)
헬퍼는 기존 Slack‑notification 패턴을 그대로 따라 백그라운드 goroutine에서 웹훅을 호출합니다.
func (h *Handler) dispatchWebhook(userID string, event webhook.Event) {
if h.webhookClient == nil {
return
}
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
webhookURL, secret, err := h.webhookClient.LookupConfigByUserID(ctx, userID)
if err != nil {
return
}
_ = h.webhookClient.Dispatch(ctx, userID, webhookURL, secret, event)
}()
}
- 이미 goroutine 안에서 실행되는 핸들러(예: 댓글 알림, 마일스톤 기록, CTA 클릭 추적)의 경우, 웹훅 클라이언트를 직접 호출합니다—추가 goroutine이 필요하지 않습니다.
URL 검증 및 비밀키 생성
- HTTPS만 허용하며, 한 가지 예외가 있습니다: 로컬 개발을 위해
http://localhost와http://127.0.0.1은 허용됩니다. - URL 길이는 500자로 제한됩니다.
- 빈 URL은 웹훅을 삭제합니다(
NULL로 저장) – 별도의 토글이 필요 없으며 Slack과 동일한 패턴을 따릅니다.
웹훅 URL을 처음 저장하고 비밀키가 없을 경우 자동으로 비밀키를 생성합니다:
func generateWebhookSecret() (string, error) {
b := make([]byte, 32) // 32 random bytes
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("generate secret: %w", err)
}
return hex.EncodeToString(b), nil
}
생성된 비밀키는 이후 모든 전송 시 X-Webhook-Signature 헤더에 사용됩니다.
TL;DR
- User‑configurable webhook URL → 이벤트당 JSON 페이로드.
- HMAC‑SHA256 signing을
X-Webhook-Signature와 함께. - Retry up to 3 times with exponential back‑off, full logging. → 지수 백오프와 전체 로깅을 적용해 최대 3회 재시도.
- Delivery rows stored in
webhook_deliveries(truncated response bodies). → 전송 기록을webhook_deliveries에 저장 (응답 본문은 잘라서 저장). - Fire‑and‑forget dispatch that never blocks the user‑facing HTTP response. → Fire‑and‑forget 방식 전송으로 사용자 응답을 차단하지 않음.
- HTTPS enforced (localhost exception) and auto‑generated secret for each user. → HTTPS 강제 적용(localhost 예외) 및 각 사용자마다 자동 생성된 비밀키.
이 모든 기능을 통해 SendRec은 어떤 다운스트림 서비스와도 통합할 수 있는 견고하고 범용적인 웹훅 시스템을 제공합니다.
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
웹훅 URL 업데이트
웹훅 URL을 업데이트할 때 기존 비밀키는 COALESCE를 사용해 보존됩니다:
INSERT INTO notification_preferences (user_id, webhook_url, webhook_secret)
VALUES ($1, $2, $3)
ON CONFLICT (user_id) DO UPDATE SET
webhook_url = $2,
webhook_secret = COALESCE(notification_preferences.webhook_secret, $3);
새 비밀키가 필요하다면 전용 “재생성” 버튼을 사용하세요. 이 버튼은 URL을 저장하는 엔드포인트와 별개의 엔드포인트를 통해 새로운 비밀키를 생성하므로, 엔드포인트를 변경할 때마다 새 비밀키를 복사할 필요가 없습니다.
Settings → Webhook Section
- URL input – 저장 버튼이 있는 필드.
- Signing secret – 기본적으로 마스킹되어 있으며, 복사 및 재생성 버튼이 있습니다.
- Send test event –
webhook.test이벤트를 발송하여 실제 뷰를 기다리지 않고 전달을 확인할 수 있습니다. - Recent deliveries – 최근 50개의 전달 시도를 표시하며, 각 항목에는:
- 이벤트 유형
- 상태 배지
- 타임스탬프
- 전체 페이로드와 응답 본문을 표시하는 확장 가능한 행
- Events reference – 모든 이벤트 유형과 해당 페이로드 필드를 나열하는 접을 수 있는 테이블.
delivery log가 가장 유용한 부분입니다: 웹훅 엔드포인트가 오류를 반환할 때, 서버 로그를 뒤지지 않고도 정확히 어떤 데이터가 전송되었고 어떤 응답이 돌아왔는지 확인할 수 있습니다.
Try It Out
SendRec는 오픈소스(AGPL‑3.0)이며 자체 호스팅이 가능합니다. 일반 웹훅은 app.sendrec.eu 에서 실시간으로 사용할 수 있습니다:
- Settings → Webhooks로 이동합니다.
- 웹훅 URL을 추가합니다.
- Send test event를 클릭합니다.
웹훅 클라이언트 구현은 webhook.go에서 확인할 수 있습니다.