우리가 SendRec에 Generic Webhooks를 추가한 방법

발행: (2026년 2월 22일 오전 02:21 GMT+9)
10 분 소요
원문: Dev.to

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://localhosthttp://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 signingX-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 eventwebhook.test 이벤트를 발송하여 실제 뷰를 기다리지 않고 전달을 확인할 수 있습니다.
  • Recent deliveries – 최근 50개의 전달 시도를 표시하며, 각 항목에는:
    • 이벤트 유형
    • 상태 배지
    • 타임스탬프
    • 전체 페이로드와 응답 본문을 표시하는 확장 가능한 행
  • Events reference – 모든 이벤트 유형과 해당 페이로드 필드를 나열하는 접을 수 있는 테이블.

delivery log가 가장 유용한 부분입니다: 웹훅 엔드포인트가 오류를 반환할 때, 서버 로그를 뒤지지 않고도 정확히 어떤 데이터가 전송되었고 어떤 응답이 돌아왔는지 확인할 수 있습니다.

Try It Out

SendRec는 오픈소스(AGPL‑3.0)이며 자체 호스팅이 가능합니다. 일반 웹훅은 app.sendrec.eu 에서 실시간으로 사용할 수 있습니다:

  1. SettingsWebhooks로 이동합니다.
  2. 웹훅 URL을 추가합니다.
  3. Send test event를 클릭합니다.

웹훅 클라이언트 구현은 webhook.go에서 확인할 수 있습니다.

0 조회
Back to Blog

관련 글

더 보기 »