01 SOFTWARE
Sdk

Webhooks

타입 안전한 Webhook 처리

Webhooks

SDK는 Next.js API Route에서 타입 안전한 webhook 처리를 제공합니다.

Webhook을 사용하면 데이터 변경 시 실시간으로 알림을 받을 수 있습니다.

기본 사용법

API Route 생성

app/api/webhook/route.ts 파일을 생성합니다.

app/api/webhook/route.ts
import { handleWebhook } from '@01.software/sdk'

export async function POST(request: Request) {
  return handleWebhook(request, async (event) => {
    console.log('Collection:', event.collection)
    console.log('Operation:', event.operation) // 'create' | 'update' | 'delete'
    console.log('Data:', event.data)

    // 이벤트 처리 로직
    if (event.collection === 'orders' && event.operation === 'create') {
      console.log('New order created:', event.data.id)
    }
  })
}

Webhook URL 등록

01.software 대시보드에서 webhook URL을 등록합니다.

https://your-domain.com/api/webhook

Webhook 이벤트

이벤트 구조

interface WebhookEvent {
  collection: string        // 컬렉션 이름
  operation: 'create' | 'update' | 'delete'  // 작업 타입
  data: any                 // 변경된 데이터
  doc: any                  // 전체 문서 (create/update)
  previousDoc?: any         // 이전 문서 (update)
}

작업 타입

  • create: 새 항목 생성
  • update: 기존 항목 수정
  • delete: 항목 삭제

타입 안전한 핸들러

특정 컬렉션에 대한 타입 안전한 핸들러를 만들 수 있습니다.

import { createTypedWebhookHandler } from '@01.software/sdk'

// 주문 전용 핸들러
const handleOrderWebhook = createTypedWebhookHandler(
  'orders',
  async (event) => {
    // event.data의 타입이 Order로 자동 추론됨
    console.log('Order:', event.data.orderNumber)
    console.log('Email:', event.data.email)
    console.log('Total:', event.data.totalAmount)

    if (event.operation === 'create') {
      // 새 주문 생성 시 처리
      await sendOrderConfirmationEmail(event.data)
    } else if (event.operation === 'update') {
      // 주문 업데이트 시 처리
      await sendOrderStatusEmail(event.data)
    }
  }
)

export async function POST(request: Request) {
  return handleWebhook(request, handleOrderWebhook)
}

여러 컬렉션 처리

Switch 문 사용

import { handleWebhook } from '@01.software/sdk'

export async function POST(request: Request) {
  return handleWebhook(request, async (event) => {
    switch (event.collection) {
      case 'orders':
        if (event.operation === 'create') {
          await handleNewOrder(event.data)
        } else if (event.operation === 'update') {
          await handleOrderUpdate(event.data, event.previousDoc)
        }
        break

      case 'products':
        if (event.operation === 'update') {
          await invalidateProductCache(event.data.id)
        }
        break

      case 'users':
        if (event.operation === 'create') {
          await sendWelcomeEmail(event.data)
        }
        break

      default:
        console.log('Unhandled collection:', event.collection)
    }
  })
}

핸들러 분리

import { handleWebhook, createTypedWebhookHandler } from '@01.software/sdk'

// 주문 핸들러
const handleOrderEvent = createTypedWebhookHandler('orders', async (event) => {
  if (event.operation === 'create') {
    await sendOrderConfirmationEmail(event.data)
    await notifyWarehouse(event.data)
  } else if (event.operation === 'update') {
    if (event.data.status !== event.previousDoc?.status) {
      await sendOrderStatusEmail(event.data)
    }
  }
})

// 상품 핸들러
const handleProductEvent = createTypedWebhookHandler('products', async (event) => {
  await invalidateProductCache(event.data.id)

  if (event.operation === 'update' && event.data.stock === 0) {
    await sendOutOfStockNotification(event.data)
  }
})

// 사용자 핸들러
const handleUserEvent = createTypedWebhookHandler('users', async (event) => {
  if (event.operation === 'create') {
    await sendWelcomeEmail(event.data)
    await createUserProfile(event.data)
  }
})

export async function POST(request: Request) {
  return handleWebhook(request, async (event) => {
    switch (event.collection) {
      case 'orders':
        return handleOrderEvent(event)
      case 'products':
        return handleProductEvent(event)
      case 'users':
        return handleUserEvent(event)
    }
  })
}

실전 예제

주문 처리

app/api/webhook/route.ts
import { handleWebhook } from '@01.software/sdk'
import { sendEmail } from '@/lib/email'
import { notifySlack } from '@/lib/slack'

export async function POST(request: Request) {
  return handleWebhook(request, async (event) => {
    if (event.collection !== 'orders') return

    const order = event.data

    switch (event.operation) {
      case 'create':
        // 주문 확인 이메일 발송
        await sendEmail({
          to: order.email,
          subject: `주문 확인 - ${order.orderNumber}`,
          template: 'order-confirmation',
          data: { order }
        })

        // Slack 알림
        await notifySlack({
          channel: '#orders',
          text: `새 주문: ${order.orderNumber} (${order.totalAmount}원)`
        })
        break

      case 'update':
        // 주문 상태 변경 시
        if (order.status !== event.previousDoc?.status) {
          await sendEmail({
            to: order.email,
            subject: `주문 상태 업데이트 - ${order.orderNumber}`,
            template: 'order-status-update',
            data: { order, previousStatus: event.previousDoc?.status }
          })
        }
        break

      case 'delete':
        // 주문 취소 처리
        await sendEmail({
          to: order.email,
          subject: `주문 취소 - ${order.orderNumber}`,
          template: 'order-cancelled',
          data: { order }
        })
        break
    }
  })
}

재고 관리

import { handleWebhook } from '@01.software/sdk'
import { serverClient } from '@/lib/server-client'

export async function POST(request: Request) {
  return handleWebhook(request, async (event) => {
    if (event.collection !== 'products') return

    const product = event.data

    // 재고 부족 알림
    if (product.stock <= product.lowStockThreshold) {
      await sendLowStockAlert(product)
    }

    // 재고 소진 처리
    if (product.stock === 0) {
      await serverClient.from('products').update(product.id, {
        status: 'out-of-stock'
      })

      await sendOutOfStockNotification(product)
    }

    // 캐시 무효화
    await invalidateProductCache(product.id)
  })
}

사용자 온보딩

import { handleWebhook } from '@01.software/sdk'
import { sendEmail } from '@/lib/email'
import { createStripeCustomer } from '@/lib/stripe'

export async function POST(request: Request) {
  return handleWebhook(request, async (event) => {
    if (event.collection !== 'users' || event.operation !== 'create') {
      return
    }

    const user = event.data

    // 환영 이메일 발송
    await sendEmail({
      to: user.email,
      subject: '가입을 환영합니다!',
      template: 'welcome',
      data: { user }
    })

    // Stripe 고객 생성
    const stripeCustomer = await createStripeCustomer({
      email: user.email,
      name: user.name,
      metadata: {
        userId: user.id
      }
    })

    // 사용자 프로필 업데이트
    await serverClient.from('users').update(user.id, {
      stripeCustomerId: stripeCustomer.id
    })
  })
}

캐시 무효화

import { handleWebhook } from '@01.software/sdk'
import { revalidatePath, revalidateTag } from 'next/cache'

export async function POST(request: Request) {
  return handleWebhook(request, async (event) => {
    switch (event.collection) {
      case 'products':
        // 상품 목록 캐시 무효화
        revalidateTag('products')
        revalidatePath('/products')

        // 특정 상품 페이지 무효화
        if (event.data.slug) {
          revalidatePath(`/products/${event.data.slug}`)
        }
        break

      case 'posts':
        // 블로그 캐시 무효화
        revalidateTag('posts')
        revalidatePath('/blog')

        if (event.data.slug) {
          revalidatePath(`/blog/${event.data.slug}`)
        }
        break

      case 'pages':
        // 페이지 캐시 무효화
        if (event.data.slug) {
          revalidatePath(`/${event.data.slug}`)
        }
        break
    }
  })
}

에러 처리

import { handleWebhook } from '@01.software/sdk'

export async function POST(request: Request) {
  return handleWebhook(request, async (event) => {
    try {
      // 이벤트 처리 로직
      await processEvent(event)
    } catch (error) {
      console.error('Webhook processing error:', error)

      // 에러 로깅 (Sentry 등)
      // Sentry.captureException(error)

      // 에러 알림
      await notifyAdmins({
        subject: 'Webhook Processing Error',
        error,
        event
      })

      // 재시도를 위해 에러를 던짐
      throw error
    }
  })
}

보안

Webhook 서명 검증

import { handleWebhook } from '@01.software/sdk'

export async function POST(request: Request) {
  // Webhook 서명 검증 (예시)
  const signature = request.headers.get('x-webhook-signature')

  if (!isValidSignature(signature, await request.text())) {
    return new Response('Invalid signature', { status: 401 })
  }

  return handleWebhook(request, async (event) => {
    // 이벤트 처리
  })
}

IP 화이트리스트

import { handleWebhook } from '@01.software/sdk'

const ALLOWED_IPS = ['1.2.3.4', '5.6.7.8']

export async function POST(request: Request) {
  const ip = request.headers.get('x-forwarded-for') || 'unknown'

  if (!ALLOWED_IPS.includes(ip)) {
    return new Response('Unauthorized', { status: 403 })
  }

  return handleWebhook(request, async (event) => {
    // 이벤트 처리
  })
}

테스트

로컬 테스트

// test/webhook.test.ts
import { POST } from '@/app/api/webhook/route'

describe('Webhook', () => {
  it('should handle order creation', async () => {
    const request = new Request('http://localhost:3000/api/webhook', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        collection: 'orders',
        operation: 'create',
        data: {
          id: 'order_123',
          orderNumber: '260107123456',
          email: 'test@example.com',
          totalAmount: 10000,
        }
      })
    })

    const response = await POST(request)
    expect(response.status).toBe(200)
  })
})

Webhook 시뮬레이션

# cURL로 webhook 테스트
curl -X POST http://localhost:3000/api/webhook \
  -H "Content-Type: application/json" \
  -d '{
    "collection": "orders",
    "operation": "create",
    "data": {
      "id": "order_123",
      "orderNumber": "260107123456",
      "email": "test@example.com"
    }
  }'

모니터링

import { handleWebhook } from '@01.software/sdk'

export async function POST(request: Request) {
  const startTime = Date.now()

  return handleWebhook(request, async (event) => {
    try {
      await processEvent(event)

      // 성공 메트릭
      const duration = Date.now() - startTime
      console.log(`Webhook processed successfully in ${duration}ms`)

      // 메트릭 전송 (DataDog, CloudWatch 등)
      // metrics.increment('webhook.success')
      // metrics.timing('webhook.duration', duration)
    } catch (error) {
      // 실패 메트릭
      console.error('Webhook failed:', error)
      // metrics.increment('webhook.failure')

      throw error
    }
  })
}

Webhook 핸들러는 빠르게 처리되어야 합니다. 긴 작업은 백그라운드 작업으로 분리하세요.

다음 단계

On this page