f8b21af4be
10_Wiki/Topics 대규모 정리: - 오류 캡처/미완성 stub 문서 227개 제거 - 교차폴더 중복 43클러스터 병합 (63파일 → redirect) - 링크명 정규화: 깨진 링크 수정·redirect 직결·개념 매핑 ~2,400건 - 카테고리 MOC 6개 신규 생성 - Graph 섹션 미해결 related-keyword 링크 10,058건 제거 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.7 KiB
5.7 KiB
id, title, category, status, canonical_id, aliases, duplicate_of, source_trust_level, confidence_score, verification_status, tags, raw_sources, last_reinforced, github_commit, tech_stack
| id | title | category | status | canonical_id | aliases | duplicate_of | source_trust_level | confidence_score | verification_status | tags | raw_sources | last_reinforced | github_commit | tech_stack | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| wiki-2026-0508-webhooks-and-notifications | WebHooks and Notifications | 10_Wiki/Topics | verified | self |
|
none | A | 0.9 | applied |
|
2026-05-10 | pending |
|
WebHooks and Notifications
매 한 줄
"매 reverse-API: 매 server 가 client 의 HTTP endpoint 로 event 를 push 하는 매 async integration pattern". 2007 GitHub 가 popularize, 2026 Stripe/Shopify/Slack 의 표준. 매 polling 대비 latency ↓ 99%, 매 challenge: delivery guarantee + signature verification + replay protection.
매 핵심
매 Webhook anatomy
POST https://your-app/hooks/stripewith JSON body + headers (Stripe-Signature,Webhook-Id,Webhook-Timestamp).- 매 receiver 의 2xx 응답 < 매 5s — 매 그렇지 않으면 sender 가 retry.
매 Delivery guarantees
- At-least-once: 매 표준. 매 idempotency key 필수.
- 매 retry: exponential backoff (1m, 5m, 30m, 2h, ...) up to 매 24-72h.
- 매 dead-letter queue + manual replay UI.
매 Security
- HMAC signature (
HMAC-SHA256(secret, timestamp + body)). - Timestamp tolerance (±5 min) → replay 방어.
- HTTPS only, IP allowlist (optional).
- 매 secret rotation 의 지원.
매 응용
- Payment events (Stripe, Toss).
- SCM events (GitHub push, PR).
- Chat platform commands (Slack, Discord).
- 매 SaaS integration hub (Zapier, n8n).
💻 패턴
Receiver (Hono + signature verify)
import { Hono } from 'hono';
import { createHmac, timingSafeEqual } from 'crypto';
const app = new Hono();
app.post('/hooks/stripe', async (c) => {
const sig = c.req.header('stripe-signature')!;
const body = await c.req.text();
const [t, v1] = sig.split(',').map(p => p.split('=')[1]);
if (Math.abs(Date.now()/1000 - +t) > 300) return c.text('stale', 400);
const expected = createHmac('sha256', process.env.STRIPE_SECRET!)
.update(`${t}.${body}`).digest('hex');
if (!timingSafeEqual(Buffer.from(expected), Buffer.from(v1)))
return c.text('bad sig', 401);
const event = JSON.parse(body);
await enqueue(event); // 매 fast 200, async process
return c.text('ok', 200);
});
Sender (with retry queue)
from svix import Svix
svix = Svix("sk_...")
svix.message.create("app_xxx", {
"event_type": "user.created",
"payload": {"id": user.id, "email": user.email},
})
# Svix handles signing, retries, replay log
Idempotency
async function handle(event: Event) {
const exists = await redis.set(
`evt:${event.id}`, '1', { NX: true, EX: 86400 }
);
if (!exists) return; // 매 already processed
await processEvent(event);
}
Retry policy
const RETRY_SCHEDULE = [60, 300, 1800, 7200, 21600, 86400]; // seconds
async function deliver(hook, attempt = 0) {
try {
const r = await fetch(hook.url, { method: 'POST', body: hook.body, headers: hook.headers });
if (r.ok) return;
throw new Error(`status ${r.status}`);
} catch (e) {
if (attempt >= RETRY_SCHEDULE.length) {
await deadLetter(hook); return;
}
await scheduleAt(deliver, hook, attempt + 1, Date.now() + RETRY_SCHEDULE[attempt] * 1000);
}
}
Push notification (FCM HTTP v1)
await fetch(`https://fcm.googleapis.com/v1/projects/${PROJECT}/messages:send`, {
method: 'POST',
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
message: {
token: deviceToken,
notification: { title: 'New message', body: msg.preview },
data: { conversationId: msg.conversationId },
android: { priority: 'HIGH' },
apns: { payload: { aps: { 'mutable-content': 1 } } },
},
}),
});
Webhook → SQS bridge (decouple)
app.post('/hooks/:provider', async c => {
const body = await c.req.text();
await sqs.send(new SendMessageCommand({
QueueUrl: process.env.HOOKS_QUEUE,
MessageBody: JSON.stringify({ provider: c.req.param('provider'), body, hdr: c.req.header() }),
}));
return c.text('ok', 200); // 매 fast ack
});
매 결정 기준
| 상황 | Approach |
|---|---|
| 매 outbound webhooks | Svix / Hookdeck (managed) |
| 매 high-volume inbound | bridge to SQS/Kafka, process async |
| Mobile push | FCM (Android+iOS) / APNs direct |
| Web push | VAPID + Service Worker |
| Internal pub/sub | NATS, Redis Streams (not webhooks) |
기본값: HMAC-SHA256 signature + idempotency + async queue + 6-step retry + dead-letter.
🔗 Graph
- 부모: Event-Driven-Architecture · HTTP-API
- 변형: WebSockets_and_Realtime · Server-Sent-Events
- 응용: Slack-Bot-Development
- Adjacent: Idempotency
🤖 LLM 활용
언제: webhook receiver scaffold, signature-verify code, retry policy boilerplate. 언제 X: secret 관리, production replay tool 설계 — domain expertise 필요.
❌ 안티패턴
- No signature verification: 매 anyone 의 spoof 가능.
- Sync heavy work in handler: 매 timeout → sender retry storm.
- No idempotency: at-least-once 의 duplicate 처리 → double-charge.
- Storing secret in code: 매 secret rotation 불가.
- No dead-letter visibility: silent failure.
🧪 검증 / 중복
- Verified (Stripe/GitHub webhook docs, Svix docs, Standard Webhooks spec 2024).
- 신뢰도 A.
🕓 Changelog
| 날짜 | 변경 |
|---|---|
| 2026-05-08 | Phase 1 |
| 2026-05-10 | Manual cleanup — webhook delivery + signature + push 패턴 |