레퍼런스개발자 레퍼런스
웹훅
슈츠에서 발생하는 이벤트를 실시간으로 수신합니다.
웹훅 개요
동작 방식
슈츠에서 이벤트 발생 (행 생성 등)
↓
웹훅 시스템이 이벤트 감지
↓
등록된 엔드포인트로 HTTP POST 요청
↓
외부 시스템에서 이벤트 처리활용 예시
- 외부 시스템 연동: 새 주문이 등록되면 ERP에 자동 동기화
- 알림: 중요 데이터 변경 시 Slack/이메일 알림
- 백업: 데이터 변경 시 외부 데이터베이스에 백업
- 워크플로우: 외부 자동화 도구(Zapier, Make) 연동
웹훅 설정
1단계: 웹훅 엔드포인트 등록
- 워크스페이스 설정 → 개발자 설정
- 웹훅 탭 선택
- + 새 웹훅 추가 클릭
2단계: 설정 입력
| 설정 | 설명 | 예시 |
|---|---|---|
| 이름 | 웹훅 식별 이름 | "주문 알림" |
| URL | 이벤트를 수신할 엔드포인트 | https://api.myapp.com/webhooks/suits |
| 이벤트 | 구독할 이벤트 유형 | row.created, row.updated |
| 데이터모델 | 특정 데이터모델만 구독 (선택) | 주문, 고객 |
3단계: 시크릿 키 저장
웹훅 시크릿 키는 한 번만 표시됩니다. 요청 서명 검증에 필요하니 안전하게 저장하세요.
whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx이벤트 유형
데이터모델 (Row) 이벤트
| 이벤트 | 설명 | 트리거 시점 |
|---|---|---|
row.created | 새 행 생성됨 | 행이 처음 생성될 때 |
row.updated | 행 수정됨 | 행의 속성이 변경될 때 |
row.deleted | 행 삭제됨 | 행이 삭제될 때 |
페이지 이벤트
| 이벤트 | 설명 | 트리거 시점 |
|---|---|---|
page.created | 새 페이지 생성됨 | 페이지가 생성될 때 |
page.updated | 페이지 수정됨 | 페이지 내용이 변경될 때 |
page.deleted | 페이지 삭제됨 | 페이지가 삭제될 때 |
워크스페이스 이벤트
| 이벤트 | 설명 | 트리거 시점 |
|---|---|---|
member.invited | 멤버 초대됨 | 새 멤버 초대 시 |
member.removed | 멤버 제거됨 | 멤버 삭제 시 |
페이로드 형식
웹훅 요청은 HTTP POST로 전송되며, JSON 형식의 페이로드를 포함합니다.
row.created
{
"id": "evt_abc123def456",
"type": "row.created",
"createdAt": "2024-03-22T10:30:00Z",
"workspaceId": "ws_xxxxx",
"data": {
"datamodelId": "dm_abc123",
"datamodelName": "고객",
"rowId": "row_xyz789",
"properties": {
"회사명": "테크스타트",
"이메일": "[email protected]",
"상태": "lead",
"계약금액": 50000000
},
"createdBy": {
"id": "user_001",
"name": "김담당",
"email": "[email protected]"
}
}
}row.updated
{
"id": "evt_def456ghi789",
"type": "row.updated",
"createdAt": "2024-03-22T11:00:00Z",
"workspaceId": "ws_xxxxx",
"data": {
"datamodelId": "dm_abc123",
"datamodelName": "고객",
"rowId": "row_xyz789",
"changes": {
"상태": {
"before": "lead",
"after": "active"
},
"계약금액": {
"before": 50000000,
"after": 55000000
}
},
"updatedBy": {
"id": "user_002",
"name": "이영업",
"email": "[email protected]"
}
}
}row.deleted
{
"id": "evt_ghi789jkl012",
"type": "row.deleted",
"createdAt": "2024-03-22T12:00:00Z",
"workspaceId": "ws_xxxxx",
"data": {
"datamodelId": "dm_abc123",
"datamodelName": "고객",
"rowId": "row_xyz789",
"deletedBy": {
"id": "user_001",
"name": "김담당"
}
}
}요청 헤더
웹훅 요청에는 다음 헤더가 포함됩니다:
POST /webhooks/suits HTTP/1.1
Host: api.myapp.com
Content-Type: application/json
User-Agent: Suits-Webhook/1.0
X-Suits-Webhook-Id: wh_xxxxx
X-Suits-Event-Id: evt_abc123def456
X-Suits-Event-Type: row.created
X-Suits-Signature: sha256=xxxxxxxx
X-Suits-Timestamp: 1711101000서명 검증
모든 웹훅 요청은 서명이 포함됩니다. 서명을 검증하여 요청이 슈츠에서 온 것인지 확인하세요.
서명 생성 방식
signature = HMAC-SHA256(timestamp + "." + payload, secret)Node.js 검증 예시
const crypto = require('crypto');
function verifyWebhookSignature(req, secret) {
const signature = req.headers['x-suits-signature'];
const timestamp = req.headers['x-suits-timestamp'];
const payload = JSON.stringify(req.body);
// 서명 검증
const signedPayload = `${timestamp}.${payload}`;
const expectedSignature = 'sha256=' +
crypto.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// 타이밍 세이프 비교
const isValid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
// 타임스탬프 검증 (5분 이내)
const currentTime = Math.floor(Date.now() / 1000);
const isRecent = currentTime - parseInt(timestamp) < 300;
return isValid && isRecent;
}
// Express 미들웨어 예시
app.post('/webhooks/suits', (req, res) => {
const isValid = verifyWebhookSignature(req, process.env.WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 이벤트 처리
const event = req.body;
console.log(`Received ${event.type} event`);
// 즉시 200 응답 (처리는 비동기로)
res.status(200).json({ received: true });
// 비동기 처리
processEvent(event);
});Python 검증 예시
import hmac
import hashlib
import time
def verify_webhook_signature(payload, signature, timestamp, secret):
# 서명 생성
signed_payload = f"{timestamp}.{payload}"
expected_signature = 'sha256=' + hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
# 비교
is_valid = hmac.compare_digest(signature, expected_signature)
# 타임스탬프 검증
is_recent = time.time() - int(timestamp) < 300
return is_valid and is_recent재시도 정책
웹훅 요청이 실패하면 자동으로 재시도합니다.
재시도 스케줄
| 시도 | 대기 시간 | 누적 시간 |
|---|---|---|
| 1차 | 1분 | 1분 |
| 2차 | 5분 | 6분 |
| 3차 | 30분 | 36분 |
| 4차 | 2시간 | 2시간 36분 |
| 5차 | 24시간 | 26시간 36분 |
실패 조건
다음 경우 요청이 실패한 것으로 간주합니다:
- 5초 이내 응답 없음 (타임아웃)
- HTTP 상태 코드가 2xx가 아닌 경우
- 연결 불가
5회 모두 실패하면 해당 이벤트는 폐기됩니다. 개발자 설정에서 실패 로그를 확인할 수 있습니다.
베스트 프랙티스
1. 빠른 응답
웹훅 엔드포인트는 5초 이내에 응답해야 합니다.
// ✅ 좋은 예: 즉시 응답, 비동기 처리
app.post('/webhook', (req, res) => {
res.status(200).json({ received: true });
// 비동기로 처리
processEventAsync(req.body);
});
// ❌ 나쁜 예: 동기 처리 후 응답
app.post('/webhook', async (req, res) => {
await heavyDatabaseOperation(); // 시간이 오래 걸림
res.status(200).json({ done: true }); // 타임아웃 위험
});2. 멱등성 보장
같은 이벤트가 여러 번 전송될 수 있습니다. 이벤트 ID로 중복을 방지하세요.
const processedEvents = new Set();
app.post('/webhook', (req, res) => {
const eventId = req.body.id;
// 이미 처리한 이벤트 스킵
if (processedEvents.has(eventId)) {
return res.status(200).json({ status: 'already_processed' });
}
processedEvents.add(eventId);
processEvent(req.body);
res.status(200).json({ received: true });
});3. 로깅
모든 웹훅 요청을 로깅하여 디버깅에 활용하세요.
app.post('/webhook', (req, res) => {
console.log({
eventId: req.body.id,
eventType: req.body.type,
timestamp: new Date().toISOString(),
payload: req.body
});
res.status(200).json({ received: true });
});4. 에러 처리
처리 중 에러가 발생해도 200을 반환하되, 내부적으로 에러를 기록하세요.
app.post('/webhook', async (req, res) => {
res.status(200).json({ received: true });
try {
await processEvent(req.body);
} catch (error) {
// 에러 로깅 (재시도하지 않음)
console.error('Webhook processing error:', error);
await alertTeam(error); // 팀에게 알림
}
});테스트
웹훅 테스트 도구
개발자 설정에서 테스트 이벤트 전송 버튼으로 웹훅을 테스트할 수 있습니다.
로컬 개발 테스트
로컬 환경에서 테스트하려면 터널링 서비스를 사용하세요:
# ngrok 사용
ngrok http 3000
# 출력된 URL을 웹훅 엔드포인트로 등록
# https://abc123.ngrok.io/webhooks/suits웹훅 로그
개발자 설정에서 최근 웹훅 요청 로그를 확인할 수 있습니다:
- 이벤트 ID
- 이벤트 유형
- 요청 시간
- 응답 상태
- 재시도 횟수
- 페이로드 (클릭하여 상세 확인)
문제 해결
웹훅이 수신되지 않음
- 엔드포인트 URL이 올바른지 확인
- 서버가 외부에서 접근 가능한지 확인
- 방화벽/보안 그룹 설정 확인
- 구독한 이벤트 유형 확인
서명 검증 실패
- 시크릿 키가 올바른지 확인
- payload를 raw string으로 사용하는지 확인
- 타임스탬프 형식 확인
타임아웃 발생
- 엔드포인트가 5초 이내에 응답하는지 확인
- 무거운 작업은 비동기로 처리
- 큐 시스템 도입 고려 (Redis, SQS 등)