Sdk
Webhooks
타입 안전한 Webhook 처리
Webhooks
SDK는 Next.js API Route에서 타입 안전한 webhook 처리를 제공합니다.
Webhook을 사용하면 데이터 변경 시 실시간으로 알림을 받을 수 있습니다.
기본 사용법
API Route 생성
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 이벤트
이벤트 구조
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)
}
})
}실전 예제
주문 처리
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 핸들러는 빠르게 처리되어야 합니다. 긴 작업은 백그라운드 작업으로 분리하세요.
다음 단계
- Error Handling - 에러 처리
- Advanced - 고급 기능