랜딩 폼, 광고 리드, 전시회 명단이 제각각 다른 시트로 들어왔습니다. 영업팀의 아침은 늘 데이터 정리로 시작됐고, 그 사이 리드는 식어갔습니다.
병목은 '옮기는 일'이었다
영업의 본질은 응대인데, 정작 시간은 취합·중복 제거·CRM 입력에 쓰였습니다. 리드가 들어와도 등록까지 하루가 걸렸고, 첫 연락은 그 뒤였습니다.
그래서 모든 채널을 하나의 흐름으로 모으고, 사람이 손대지 않아도 CRM까지 닿게 만들었습니다.
파이프라인 구성
- 모든 유입 채널을 웹훅 하나로 통합 수집
- 이메일·전화번호 기준으로 중복 자동 병합
- 리드 점수·소스 태그를 붙여 CRM에 실시간 등록
- 신규 리드가 생기면 담당자에게 즉시 알림
중복 판정 — 같은 사람을 어떻게 알아보나
"이메일·전화번호 기준으로 병합"은 한 줄로 쓰기 쉽지만, 실제로는 여기서 대부분의 시간이 사라집니다. 같은 사람이 다른 문자열로 들어오기 때문입니다. 이 워크플로우가 실제로 쓰는 규칙은 아래 세 가지입니다.
- 이메일 정규화 — 앞뒤 공백을 제거하고 전부 소문자로 바꾼 뒤,
+뒤의 태그를 떼어냅니다.Hans+ads@Example.com과hans@example.com은 같은 사람입니다. 광고 채널별로+tag를 붙여 쓰는 팀이라면 이 한 줄이 중복의 절반을 없앱니다. - 전화번호 정규화 — 숫자만 남깁니다. 하이픈·괄호·공백을 전부 제거하고,
+82또는82로 시작하면 국가번호를 떼고 앞에0을 붙입니다.+82 10 1234 5678,010-1234-5678,(010) 1234 5678이 모두01012345678이 됩니다. - 병합 우선순위 — 정규화된 이메일이 있으면 그것을 식별자로 씁니다. 없으면 정규화된 전화번호를 씁니다. 둘 다 없으면 리드로 치지 않고 `needs_review`로 표시해 따로 뺍니다. 조용히 버리면 안 됩니다 — 폼이 고장 났을 때 알 방법이 사라집니다.
필요한 것
- n8n — 자체 호스팅 또는 Cloud. 폼과 광고 플랫폼이 웹훅을 쏴야 하므로 인터넷에서 접근 가능한 주소가 필요합니다.
- Postgres 데이터베이스 하나 — 리드 테이블 하나를 만듭니다. 중복 방지를 데이터베이스 제약조건으로 거는 것이 이 설계의 핵심입니다.
- Postgres 자격증명 — n8n → Credentials →
Postgres. n8n이 Docker 안이고 DB가 호스트 머신이면 호스트에host.docker.internal을 넣으세요. - Slack Bot User OAuth Token —
chat:write,channels:read스코프. 새 리드 알림용입니다. - 리드를 보내줄 폼 또는 광고 채널 — 대부분의 폼 빌더와 광고 플랫폼이 웹훅 전송을 지원합니다. 웹훅이 없다면 그 채널만 별도 수집 노드를 붙이면 됩니다.
- CRM은 선택입니다 — 아래 'CRM 선택' 항목을 먼저 읽어보세요. 시트나 DB만으로 시작해도 이 워크플로우는 완결됩니다.
1단계 — 리드 테이블 만들기
CREATE TABLE IF NOT EXISTS leads (
dedupe_key text PRIMARY KEY, -- 정규화된 이메일 또는 전화번호. 중복 방어의 핵심
name text,
email text,
phone text,
company text,
message text,
source text, -- landing / ads / expo ...
utm_source text,
utm_campaign text,
touch_count integer NOT NULL DEFAULT 1, -- 같은 사람이 몇 번 들어왔나
created_at timestamptz NOT NULL DEFAULT now(),
last_seen_at timestamptz NOT NULL DEFAULT now()
);dedupe_key를 PRIMARY KEY로 둔 것이 중요합니다. 중복을 막는 것은 워크플로우 코드가 아니라 데이터베이스입니다. 워크플로우에 버그가 있어도, 폼이 같은 요청을 두 번 보내도, 광고 플랫폼이 재시도를 해도 행은 하나입니다. touch_count는 같은 사람이 몇 번 다시 왔는지를 알려주는데, 이 숫자가 높은 리드는 대체로 관심도가 높습니다.2단계 — 병합 쿼리
INSERT INTO leads
(dedupe_key, name, email, phone, company, message, source, utm_source, utm_campaign)
VALUES
('{{ $json.dedupe_key }}', '{{ $json.name }}', '{{ $json.email }}',
'{{ $json.phone }}', '{{ $json.company }}', '{{ $json.message }}',
'{{ $json.source }}', '{{ $json.utm_source }}', '{{ $json.utm_campaign }}')
ON CONFLICT (dedupe_key) DO UPDATE SET
name = COALESCE(EXCLUDED.name, leads.name),
company = COALESCE(EXCLUDED.company, leads.company),
last_seen_at = now(),
touch_count = leads.touch_count + 1
RETURNING dedupe_key, name, email, phone, company, source,
touch_count, (xmax = 0) AS is_new;COALESCE(EXCLUDED.name, leads.name)는 새로 온 값이 비어 있으면 기존 값을 지키라는 뜻입니다. 전시회 명단처럼 이름만 있고 회사가 빈 데이터가 나중에 들어와도, 이미 채워둔 회사명이 지워지지 않습니다.
(xmax = 0) AS is_new가 이 쿼리에서 가장 유용한 한 줄입니다. Postgres에서 이 값이 참이면 방금 INSERT된 신규 행, 거짓이면 기존 행이 UPDATE된 것입니다. 덕분에 재유입 리드는 기록만 갱신하고 알림은 진짜 신규일 때만 보낼 수 있습니다. 이게 없으면 같은 사람이 광고를 세 번 클릭할 때마다 영업팀 Slack이 세 번 울립니다.승인 게이트가 필요한 경우
리드가 들어오자마자 자동으로 메일이 나가는 구성은 위험할 수 있습니다. 실제 발주에서도 "내가 승인하기 전에는 아무것도 발송되지 않게 해달라"는 요구가 흔합니다. 자동 발송을 원하지 않는다면 이렇게 바꾸세요.
Alert Only If New노드 뒤에 발송 노드를 바로 잇지 말고,leads테이블에approved boolean DEFAULT false컬럼을 추가합니다.- Slack 알림 메시지에 담당자가 확인할 정보(회사·문의 내용)를 실어 보냅니다.
- 담당자가 확인 후 승인하면
approved를true로 바꾸고, 별도 스케줄 워크플로우가 승인된 건만 골라 발송합니다. - 승인 절차 자체를 자동화하고 싶다면 사내 메신저 버튼으로 승인받는 방식이 있습니다 — 저희
telegram-approval케이스가 그 구조를 다룹니다.
CRM 선택 — HubSpot이 정답은 아닙니다
이 워크플로우에는 CRM 연동 노드를 일부러 넣지 않았습니다. 시장에 따라 정답이 다르기 때문입니다.
- 해외 B2B SaaS라면 HubSpot·Salesforce·Pipedrive가 자연스럽습니다. HTTP Request 노드 하나를 추가해 각 CRM의 컨택 생성 API를 호출하면 됩니다.
- 국내 중소기업이라면 대부분 그런 CRM을 쓰지 않습니다. 실제로는 구글 시트, Notion, 채널톡, 또는 사내 시스템입니다. 안 쓰는 CRM에 맞춰 워크플로우를 짜면 그 워크플로우는 안 쓰입니다.
- 아무것도 없다면 그대로 두세요.
leads테이블 자체가 이미 CRM의 최소 기능을 합니다. 조회는 SQL로, 공유는 시트 내보내기로 시작하고, 필요해진 시점에 CRM을 붙이면 됩니다. 처음부터 CRM을 정하려다 프로젝트가 멈추는 경우가 더 많습니다.
+태그 이메일, 하이픈/괄호/공백 전화, +82와 82 국가번호, 이메일·전화 동시 존재 시 우선순위, 그리고 식별 불가 세 경우(둘 다 없음 / @ 없는 문자열 / 너무 짧은 번호)가 needs_review로 분리되는 것까지 전부 통과했습니다. 병합 쿼리는 PostgreSQL 17에서 같은 리드를 세 번 넣어 행은 하나만 남고 `touch_count`가 1→2→3으로 증가하며 `is_new`가 첫 회에만 참임을 확인했습니다. 노드 타입·버전은 기존 배포 워크플로우와 동일한 조합입니다. 실제 Slack 게시와 실제 폼·광고 플랫폼 연동은 확인하지 않았습니다. 마지막 검증: 2026-08-15.설정 순서 (20분)
- 테이블 생성 — 1단계 SQL을 DB에서 실행합니다.
- Postgres 자격증명 등록 — n8n → Credentials →
Postgres→ Test로 초록불 확인. - Slack 자격증명 등록 — api.slack.com/apps에서 앱 생성 → OAuth & Permissions →
chat:write,channels:read추가 → Install to Workspace →xoxb-토큰을 n8n → Credentials →Slack API에 등록. - 봇 초대 — 알림 받을 채널에서
/invite @봇이름. - 워크플로우 import — 다운로드한 JSON을 n8n → Workflows →
...→ Import from File. - 채널 이름 교체 + 자격증명 연결 —
Notify Sales노드의REPLACE_WITH_YOUR_CHANNEL_NAME_OR_ID를 실제 채널명으로 바꾸고, 두 노드에 자격증명을 붙입니다. - Activate — 켜야 Production 웹훅 주소가 살아납니다.
- 웹훅 주소 복사 —
Lead Webhook노드를 열어 Production URL을 복사합니다. - 폼·광고 채널 연결 — 각 폼 빌더/광고 플랫폼의 웹훅 설정에 그 주소를 넣습니다. 보내는 필드 이름은
name,email,phone,company,message,source를 권장합니다. 이름이 다르면Normalize노드에서 매핑을 바꾸면 됩니다. - 중복 테스트 — 같은 이메일로 폼을 두 번 제출해 보세요. Slack 알림은 한 번만 와야 하고,
SELECT dedupe_key, touch_count FROM leads;에서touch_count가 2가 돼 있어야 합니다. 이 확인을 꼭 하세요.
예외 상황이 있으면 어떻게 되나
- 이메일 없이 전화만 온 리드 — 전화번호를 식별자로 씁니다. 정상 처리됩니다.
- 이메일도 전화도 없는 제출 —
needs_review: true로 표시됩니다. 조용히 버리지 않으므로 폼이 깨졌을 때 눈에 띕니다. 이 건들을 주기적으로 확인하거나 별도 채널로 알림을 거세요. - 같은 사람이 다른 이메일로 문의 — 두 건으로 남습니다. 자동으로는 풀 수 없습니다. 영업 담당자가 수동으로 병합할 수 있게 해두세요.
- 광고 스팸 리드 — 이 워크플로우는 스팸 필터가 아닙니다. 유입량이 많다면
source별로 나눠 보고, 특정 소스만 승인 게이트를 태우는 방식이 현실적입니다. - CRM API가 다운된 경우 — CRM 연동을 추가했다면 그 호출이 실패해도 리드는 이미 `leads` 테이블에 안전하게 들어가 있습니다. DB 저장을 CRM 전송보다 먼저 두는 이유가 이것입니다. 나중에 미전송 건만 골라 다시 보내면 됩니다.
- 폼이 같은 요청을 두 번 보냄 —
ON CONFLICT가 막습니다.touch_count만 올라갑니다.
리드가 들어오면 슬랙이 저보다 먼저 압니다. 식기 전에 전화 돌립니다.— 세일즈 리드