TL;DR: 업데이트 확인은 주기적으로 폴링(간단하지만 비효율적)합니다. 웹훅은 업데이트를 실시간으로 푸시합니다(효율적이지만 복잡). 드문 확인에는 폴링을, 실시간 업데이트에는 웹훅을 사용하세요. Modern PetstoreAPI는 안정적인 웹훅 전달을 통해 두 가지 패턴을 모두 지원합니다.
차이점 이해하기
폴링(Polling): 클라이언트가 "새 업데이트 있나요?"라고 반복해서 묻습니다. 웹훅(Webhooks): 어떤 일이 발생하면 서버가 "여기 업데이트가 있습니다!"라고 알려줍니다.
비유:
- 폴링 = 한 시간마다 우편함 확인하기
- 웹훅 = 우편물이 도착하면 우편 배달부가 초인종 누르기
폴링: 작동 방식
클라이언트는 변경 사항을 확인하기 위해 주기적으로 요청을 보냅니다.
// Poll every 30 seconds
setInterval(async () => {
const response = await fetch('https://petstoreapi.com/api/v1/orders/123');
const order = await response.json();
if (order.status === 'completed') {
console.log('Order completed!', order);
clearInterval(pollInterval);
}
}, 30000);
폴링 패턴:
단순 폴링:
GET /api/v1/orders/123
# Returns current order state
조건부 폴링 (ETag):
GET /api/v1/orders/123
If-None-Match: "abc123"
# Returns 304 Not Modified if unchanged
# Returns 200 with new data if changed
시점 기반 폴링 (Since-based polling):
GET /api/v1/orders/123/events?since=1710331200
# Returns events since timestamp
웹훅: 작동 방식
이벤트가 발생하면 서버가 사용자의 엔드포인트로 HTTP POST를 보냅니다.
설정 흐름:
// 1. Register webhook endpoint
POST /api/v1/webhooks
{
"url": "https://myapp.com/webhooks/petstore",
"events": ["order.created", "order.completed"],
"secret": "whsec_abc123"
}
// 2. Server sends webhook when event occurs
POST https://myapp.com/webhooks/petstore
{
"id": "evt_123",
"type": "order.completed",
"created": 1710331200,
"data": {
"orderId": "123",
"status": "completed",
"completedAt": "2024-01-01T12:00:00Z"
}
}
// 3. Verify and process webhook
// Respond with 200 OK
폴링은 언제 사용해야 할까요?
적합한 경우:
- 잦지 않은 확인 (시간당 1회)
- 적은 수의 리소스
- 간단한 구현
- 클라이언트를 제어하는 경우
- 테스트 및 디버깅
예시:
- 일일 보고서 상태 확인
- 몇 분마다 연락처 동기화
- 서버 상태 모니터링
- 결제 상태 확인 (자주 발생하지 않는 경우)
폴링이 괜찮은 경우:
- 업데이트가 드물 때
- 약간의 지연이 허용될 때
- 간단한 구현을 원할 때
- 리소스가 작을 때
웹훅은 언제 사용해야 할까요?
적합한 경우:
- 실시간 업데이트
- 모니터링할 리소스가 많을 때
- 시간에 민감한 이벤트
- 타사 통합
- 잦은 업데이트
예시:
- 결제 확인
- 채팅 메시지
- 주가 알림
- 주문 상태 변경
- CI/CD 알림
웹훅이 더 나은 경우:
- 업데이트가 즉시 필요할 때
- 폴링이 비효율적일 때
- 많은 클라이언트가 동일한 리소스를 모니터링할 때
- 서버 부하를 줄이고 싶을 때
비교표
| 요소 | 폴링 | 웹훅 |
|---|---|---|
| 지연 시간 | 폴링 간격만큼 | 실시간 |
| 서버 부하 | 높음 (많은 비어있는 요청) | 낮음 (실제 이벤트만) |
| 복잡성 | 간단함 | 복잡함 |
| 신뢰성 | 높음 (클라이언트가 재시도 제어) | 중간 (재시도 로직 필요) |
| 설정 | 없음 | 엔드포인트 등록 |
| 방화벽 문제 | 없음 (아웃바운드 전용) | 화이트리스트 지정 필요 가능성 |
| 비용 | 높음 (더 많은 요청) | 낮음 (더 적은 요청) |
| 가장 적합한 경우 | 드문 확인 | 실시간 업데이트 |
폴링 구현하기
기본 폴링
async function pollOrderStatus(orderId, callback) {
let lastStatus = null;
const poll = async () => {
try {
const response = await fetch(`https://petstoreapi.com/api/v1/orders/${orderId}`);
const order = await response.json();
// Only callback if status changed
if (order.status !== lastStatus) {
lastStatus = order.status;
callback(order);
}
// Stop polling if terminal state
if (['completed', 'cancelled'].includes(order.status)) {
return;
}
// Continue polling
setTimeout(poll, 5000);
} catch (error) {
console.error('Polling error:', error);
setTimeout(poll, 30000); // Back off on error
}
};
poll();
}
// Usage
pollOrderStatus('order-123', (order) => {
console.log(`Order status: ${order.status}`);
});
스마트 폴링 (지수 백오프)
async function smartPoll(url, callback, options = {}) {
const {
maxRetries = 10,
initialInterval = 1000,
maxInterval = 60000,
stopCondition = () => false
} = options;
let retries = 0;
let interval = initialInterval;
let lastData = null;
const poll = async () => {
try {
const response = await fetch(url);
const data = await response.json();
// Callback if data changed
if (JSON.stringify(data) !== JSON.stringify(lastData)) {
lastData = data;
callback(data);
}
// Stop if condition met
if (stopCondition(data)) {
return;
}
// Reset interval on successful request
interval = initialInterval;
} catch (error) {
retries++;
if (retries >= maxRetries) {
throw new Error('Max retries exceeded');
}
}
// Schedule next poll with exponential backoff
setTimeout(poll, interval);
interval = Math.min(interval * 2, maxInterval);
};
poll();
}
// Usage: Poll order until completed
smartPoll('https://petstoreapi.com/api/v1/orders/123',
(order) => console.log('Order:', order),
{
stopCondition: (order) => ['completed', 'cancelled'].includes(order.status),
initialInterval: 2000,
maxInterval: 30000
}
);
ETag를 사용한 폴링
async function pollWithEtag(url, callback) {
let etag = null;
const poll = async () => {
const headers = {};
if (etag) {
headers['If-None-Match'] = etag;
}
const response = await fetch(url, { headers });
if (response.status === 304) {
// Not modified, continue polling
setTimeout(poll, 30000);
return;
}
const data = await response.json();
etag = response.headers.get('etag');
callback(data);
setTimeout(poll, 30000);
};
poll();
}
웹훅 구현하기
웹훅 등록하기
// Register webhook endpoint
async function registerWebhook(url, events) {
const response = await fetch('https://petstoreapi.com/api/v1/webhooks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({
url,
events,
secret: generateSecret()
})
});
return response.json();
}
function generateSecret() {
return 'whsec_' + crypto.randomBytes(32).toString('hex');
}
웹훅 수신하기
const express = require('express');
const crypto = require('crypto');
const app = express();
// Raw body parser for signature verification
app.use('/webhooks', express.raw({ type: 'application/json' }));
app.post('/webhooks/petstore', async (req, res) => {
const signature = req.headers['x-petstore-signature'];
const body = req.body;
// Verify signature
const isValid = verifySignature(body, signature, process.env.WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(body.toString());
// Process event
switch (event.type) {
case 'order.created':
await handleOrderCreated(event.data);
break;
case 'order.completed':
await handleOrderCompleted(event.data);
break;
case 'order.cancelled':
await handleOrderCancelled(event.data);
break;
}
// Acknowledge receipt
res.status(200).json({ received: true });
});
function verifySignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
로컬에서 웹훅 테스트하기
# Use ngrok to expose local endpoint
ngrok http 3000
# Register ngrok URL as webhook endpoint
curl -X POST https://petstoreapi.com/api/v1/webhooks \
-H "Authorization: Bearer $TOKEN" \
-d '{
"url": "https://abc123.ngrok.io/webhooks/petstore",
"events": ["order.created", "order.completed"]
}'
안정적인 웹훅 전달
웹훅은 실패할 수 있습니다. 재시도 로직을 구현하세요.
발신자 측 (서버)
// Queue webhooks for delivery
const webhookQueue = [];
async function sendWebhook(event) {
const webhooks = await db.webhooks.findMany({
where: { events: { contains: event.type } }
});
for (const webhook of webhooks) {
webhookQueue.push({
webhook,
event,
attempts: 0,
nextAttempt: Date.now()
});
}
processQueue();
}
async function processQueue() {
const now = Date.now();
for (const item of webhookQueue) {
if (item.nextAttempt > now) continue;
try {
await deliverWebhook(item);
// Remove from queue on success
webhookQueue.splice(webhookQueue.indexOf(item), 1);
} catch (error) {
// Schedule retry with exponential backoff
item.attempts++;
item.nextAttempt = now + getBackoff(item.attempts);
if (item.attempts >= 5) {
// Mark as failed after 5 attempts
await markWebhookFailed(item);
webhookQueue.splice(webhookQueue.indexOf(item), 1);
}
}
}
setTimeout(processQueue, 5000);
}
function getBackoff(attempt) {
// 1min, 5min, 15min, 1hr, 4hr
const delays = [60000, 300000, 900000, 3600000, 14400000];
return delays[attempt - 1] || delays[delays.length - 1];
}
async function deliverWebhook({ webhook, event }) {
const signature = generateSignature(event, webhook.secret);
const response = await fetch(webhook.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Petstore-Signature': signature,
'X-Petstore-Event': event.type
},
body: JSON.stringify(event),
timeout: 10000
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
}
수신자 측 (클라이언트)
// Idempotent webhook handling
const processedEvents = new Set();
app.post('/webhooks/petstore', async (req, res) => {
const event = JSON.parse(req.body.toString());
// Skip if already processed (idempotency)
if (processedEvents.has(event.id)) {
return res.status(200).json({ received: true });
}
try {
await processEvent(event);
processedEvents.add(event.id);
// Clean up old event IDs (keep last 1000)
if (processedEvents.size > 1000) {
const arr = Array.from(processedEvents);
arr.slice(0, arr.length - 1000).forEach(id => processedEvents.delete(id));
}
res.status(200).json({ received: true });
} catch (error) {
console.error('Webhook processing error:', error);
// Return 5xx to trigger retry
res.status(500).json({ error: 'Processing failed' });
}
});
async function processEvent(event) {
// Process the event
switch (event.type) {
case 'order.created':
await handleOrderCreated(event.data);
break;
// ... handle other events
}
}
하이브리드 접근 방식
중요한 업데이트에는 폴링과 웹훅을 모두 사용하세요.
class OrderMonitor {
constructor(orderId, callback) {
this.orderId = orderId;
this.callback = callback;
this.pollInterval = null;
}
async start() {
// Start with polling for immediate feedback
this.startPolling();
// Register webhook for real-time update
await this.registerWebhook();
}
startPolling() {
this.pollInterval = setInterval(async () => {
const order = await this.fetchOrder();
this.callback(order);
if (['completed', 'cancelled'].includes(order.status)) {
this.stop();
}
}, 10000);
}
async registerWebhook() {
const response = await fetch('https://petstoreapi.com/api/v1/webhooks', {
method: 'POST',
headers: { 'Authorization': `Bearer ${TOKEN}` },
body: JSON.stringify({
url: 'https://myapp.com/webhooks/petstore',
events: [`order.${this.orderId}`],
oneTime: true // Auto-delete after first delivery
})
});
this.webhookId = (await response.json()).id;
}
stop() {
if (this.pollInterval) {
clearInterval(this.pollInterval);
}
if (this.webhookId) {
fetch(`https://petstoreapi.com/api/v1/webhooks/${this.webhookId}`, {
method: 'DELETE'
});
}
}
}
자주 묻는 질문 (FAQ)
Q: 폴링은 얼마나 자주 해야 하나요?긴급성에 따라 다릅니다. 거의 실시간이라면 30초, 긴급하지 않다면 5분. 최신 상태 유지와 서버 부하 사이의 균형을 맞추세요.
Q: 웹훅 엔드포인트가 다운되면 어떻게 되나요?훌륭한 웹훅 제공자는 지수 백오프를 사용하여 재시도를 합니다. 중복 전달을 처리하기 위해 멱등성(idempotency)을 구현하세요.
Q: 웹훅을 어떻게 보호하나요?공유 비밀 키를 사용하여 서명을 확인하세요. HTTPS만 사용하고, 이벤트 데이터를 검증하세요.
Q: 웹훅을 과거 데이터에 사용할 수 있나요?아니요. 웹훅은 새 이벤트에만 사용됩니다. 과거 데이터에는 폴링 또는 배치(batch) API를 사용하세요.
Q: 모바일 앱에는 폴링 또는 웹훅 중 어떤 것을 사용해야 하나요?모바일에서는 폴링이 더 간단합니다. 웹훅은 중간에 푸시 알림이 필요합니다.
Q: 웹훅 문제를 어떻게 디버깅하나요?테스트를 위해 webhook.site와 같은 도구를 사용하세요. 모든 웹훅 전달을 로깅하세요. API에 웹훅 이벤트 기록을 제공하세요.
Modern PetstoreAPI는 폴링과 웹훅을 모두 지원합니다. 구현 세부 사항은 웹훅 가이드를 참조하세요. Apidog를 사용하여 웹훅 통합을 테스트하세요.
