웹훅은 '무슨 일이 생기면 알려줘'를 구현하는 가장 짧은 코드입니다. 폼 제출, 결제 완료, 새 가입 — 외부 서비스의 사건 하나를 워크플로우의 시작점으로 삼을 수 있습니다. 폴링도, 크론도, 배치도 필요 없습니다.
이 글은 n8n의 Webhook 노드를 처음 다루는 사람을 위한 실전 가이드입니다. URL을 어떻게 발급받고, 어떤 데이터가 들어오는지 확인하고, 서명 검증까지 붙이는 전 과정을 다룹니다.
웹훅이 뭘 바꾸는가
폴링 방식은 '1분마다 확인'입니다. 웹훅은 '일이 생기는 순간 알림이 온다'입니다. 지연 시간이 60초에서 200ms로 줄어들고, 서버는 놀고 있을 때 완전히 멈춰 있습니다. 트래픽도, 전력도, 마음도 아낍니다.
동작 방식 — URL 하나면 끝
n8n 캔버스에 Webhook 노드를 하나 두면 두 개의 URL이 발급됩니다. 하나는 테스트용, 하나는 프로덕션용입니다. 외부 서비스가 이 URL로 HTTP 요청을 보내는 순간 워크플로우가 시작됩니다.
5분 만에 첫 웹훅 받기
- 빈 워크플로우에
Webhook노드 추가 - HTTP Method는
POST, Path는orders로 설정 - 우측 상단에서 Listen for Test Event 클릭
- 터미널에서 아래 curl 명령으로 테스트 데이터 전송
- 노드에 들어온 JSON을 확인하고 Save
curl -X POST \
https://your-n8n.example.com/webhook-test/orders \
-H 'Content-Type: application/json' \
-d '{"order_id": 1024, "amount": 89000, "status": "paid"}'들어온 데이터를 다루는 법
다음 노드에서 표현식으로 {{ $json.order_id }} 같이 참조합니다. 헤더까지 필요하면 Webhook 노드 설정에서 Response 섹션의 옵션을 켜서 $json.headers, $json.body, $json.query로 분리해 받을 수 있습니다.
$json— 요청 바디 (기본)$json.headers— 헤더 전체$json.query— 쿼리 스트링$binary— 파일 업로드 시 원본 바이너리
보안은 선택이 아니다
공개된 URL은 누구나 쏠 수 있다는 뜻입니다. 최소 세 겹의 방어선을 두세요. 대부분의 침해는 이 셋 중 하나만 있어도 막힙니다.
- 추측 불가능한 경로 —
orders대신orders-8f3a2b1c같이 토큰 포함 - 서명 검증 — Stripe, GitHub 등이 보내는
X-Signature헤더를 HMAC으로 대조 - Basic Auth 또는 헤더 토큰 — 노드 설정의 Authentication 옵션 활용
// Code 노드에서 HMAC 서명 검증
// 사전 준비:
// 1) Webhook 노드 옵션에서 "Raw Body"(또는 Binary Data) 활성화 → 원본 바이트가 $binary.data 에 저장됨.
// 2) 셀프호스팅 n8n이라면 컨테이너 환경변수 NODE_FUNCTION_ALLOW_BUILTIN=crypto 를 설정해야
// require('crypto')가 Code 노드에서 동작합니다.
const crypto = require('crypto');
const secret = $env.WEBHOOK_SECRET;
const provided = ($json.headers['x-signature'] || '').replace(/^sha256=/, '');
// JSON.stringify($json.body)는 재직렬화된 결과라 발신자가 서명한 원본 바이트와 다를 수 있음.
// 항상 원본 요청 바이트로 검증해야 함:
const raw = Buffer.from($binary.data.data, 'base64');
const expected = crypto.createHmac('sha256', secret).update(raw).digest('hex');
// 길이가 다르면 timingSafeEqual이 예외를 던지므로 길이부터 가드:
const a = Buffer.from(provided, 'hex');
const b = Buffer.from(expected, 'hex');
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
throw new Error('bad signature');
}
return $input.all();응답을 어떻게 돌려줄까
기본값은 즉시 200 OK입니다. 문제는 외부 서비스가 응답 본문을 기대할 때입니다. 이럴 땐 Webhook 노드의 Response Mode를 Using 'Respond to Webhook' Node로 바꾸고, 마지막에 Respond to Webhook 노드를 두어 원하는 상태 코드와 JSON을 직접 반환하세요.
웹훅을 이해하면 '연동'이라는 단어가 더는 무섭지 않다. 대부분의 연결은 결국 URL 하나에서 시작된다.— SynAct.ai 기술팀
자주 밟는 함정
- 테스트 URL로 프로덕션을 연결해 두고 워크플로우를 비활성화 — 조용히 실패한다
- 응답이 30초를 넘겨 상대 서비스가 재시도 폭탄을 던진다
- 재시도 시 같은 이벤트가 여러 번 들어오는데 멱등성 처리가 없다
특히 세 번째는 결제, 세금계산서 같은 도메인에서 치명적입니다. order_id 같은 식별자를 키로 잡고 이미 처리했는지 먼저 확인하는 습관을 들이세요.
자주 묻는 질문
- Q. 테스트 URL과 프로덕션 URL의 차이가 뭔가요? A. 테스트 URL은 워크플로우가 비활성 상태에서도 동작하며, 캔버스의 'Listen for Test Event'를 누른 뒤 한 번의 요청만 받아 캔버스에 값을 채웁니다. 프로덕션 URL은 워크플로우가 Active 상태에서만 응답하며, 오는 요청마다 실행됩니다. 배포 직후 '왜 안 되지?'의 90%는 이걸 잊어서 생깁니다.
- Q. 웹훅 응답이 지연되면 발신 측이 재시도하나요? A. 대부분의 서비스(Stripe · GitHub · Slack)는 15-30초를 기다리다 200 OK가 오지 않으면 재시도를 보냅니다. 무거운 처리는 즉시 200으로 응답한 뒤 백그라운드로 처리하고, 응답 본문이 필요한 경우에만
Respond to Webhook노드를 쓰세요. - Q. 서명 검증(HMAC)은 언제 필수인가요? A. 외부 서비스가 서명 헤더를 보내는 경우(Stripe · GitHub · Shopify) 반드시 검증하세요. 결제 · 발송 · 개인정보 처리처럼 되돌리기 어려운 액션을 웹훅으로 트리거한다면 자체 서비스여도 서명은 필수. 위 코드 블록에 검증 스니펫이 있습니다.
- Q. 동일 이벤트가 중복 발송되는 경우 어떻게 처리하나요? A. 발신 측 재시도의 결과일 수 있으므로
order_id같은 이벤트 식별자를 키로 잡고 이미 처리했는지 먼저 확인하는 멱등성(idempotency) 처리가 필요합니다. Redis · Postgres에 처리한 ID를 남기거나 DB의 UNIQUE 제약조건으로 자연스럽게 막는 방식. - Q. Basic Auth와 서명 검증, 둘 다 필요한가요? A. Basic Auth는 '누가 보내는지'를, 서명 검증은 '내용이 변조되지 않았는지'를 확인합니다. 성격이 다르므로 둘 다 두는 것이 안전. 특히 결제 · 관리자 트리거는 두 겹 방어 권장.
- Q. n8n Cloud와 self-hosted에서 webhook URL이 다른가요? A. 경로 형식은 동일합니다(
/webhook/<path>또는/webhook-test/<path>). 도메인만 각각의 인스턴스 주소로 바뀝니다. Cloud는https://<workspace>.app.n8n.cloud/webhook/..., self-hosted는 여러분의 도메인.
다음 단계
웹훅 하나로 트리거를 잡았다면 이제 남은 건 조건 분기와 액션입니다. IF/Switch로 흐름을 나누고, Slack이나 ERP로 붙이면 그것이 곧 실무에서 말하는 '자동화'입니다. 한 번 만들어 두면 다시 손댈 일이 거의 없습니다.
NODE_FUNCTION_ALLOW_BUILTIN=crypto 활성화)에서 실제 curl 요청 · Stripe 스타일 HMAC 서명으로 왕복 검증했습니다. 마지막 검증: 2026-01-21.