웹훅 vs 폴링: 어떤 API 통합 방식이 더 좋을까?

Ashley Innocent

Ashley Innocent

20 March 2026

웹훅 vs 폴링: 어떤 API 통합 방식이 더 좋을까?

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

폴링은 언제 사용해야 할까요?

적합한 경우:

예시:

폴링이 괜찮은 경우:

웹훅은 언제 사용해야 할까요?

적합한 경우:

예시:

웹훅이 더 나은 경우:

비교표

요소 폴링 웹훅
지연 시간 폴링 간격만큼 실시간
서버 부하 높음 (많은 비어있는 요청) 낮음 (실제 이벤트만)
복잡성 간단함 복잡함
신뢰성 높음 (클라이언트가 재시도 제어) 중간 (재시도 로직 필요)
설정 없음 엔드포인트 등록
방화벽 문제 없음 (아웃바운드 전용) 화이트리스트 지정 필요 가능성
비용 높음 (더 많은 요청) 낮음 (더 적은 요청)
가장 적합한 경우 드문 확인 실시간 업데이트

폴링 구현하기

기본 폴링

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를 사용하여 웹훅 통합을 테스트하세요.

Apidog에서 API 설계-첫 번째 연습

API를 더 쉽게 구축하고 사용하는 방법을 발견하세요