케이스 스터디
CASE STUDY — 마케팅

리드 수집 자동화 — 중복을 CRM에 넣기 전에 걸러내는 법

도입 B2B SaaS구축 5일
WebhookPostgresSlackCRM(선택)

랜딩 폼, 광고 리드, 전시회 명단이 제각각 다른 시트로 들어왔습니다. 영업팀의 아침은 늘 데이터 정리로 시작됐고, 그 사이 리드는 식어갔습니다.

병목은 '옮기는 일'이었다

영업의 본질은 응대인데, 정작 시간은 취합·중복 제거·CRM 입력에 쓰였습니다. 리드가 들어와도 등록까지 하루가 걸렸고, 첫 연락은 그 뒤였습니다.

그래서 모든 채널을 하나의 흐름으로 모으고, 사람이 손대지 않아도 CRM까지 닿게 만들었습니다.

lead-capture.workflowLIVE
FM폼 · 광고GS수집 · 정리DD중복 제거CRMCRM 동기화

파이프라인 구성

  • 모든 유입 채널을 웹훅 하나로 통합 수집
  • 이메일·전화번호 기준으로 중복 자동 병합
  • 리드 점수·소스 태그를 붙여 CRM에 실시간 등록
  • 신규 리드가 생기면 담당자에게 즉시 알림

중복 판정 — 같은 사람을 어떻게 알아보나

"이메일·전화번호 기준으로 병합"은 한 줄로 쓰기 쉽지만, 실제로는 여기서 대부분의 시간이 사라집니다. 같은 사람이 다른 문자열로 들어오기 때문입니다. 이 워크플로우가 실제로 쓰는 규칙은 아래 세 가지입니다.

  1. 이메일 정규화 — 앞뒤 공백을 제거하고 전부 소문자로 바꾼 뒤, + 뒤의 태그를 떼어냅니다. Hans+ads@Example.comhans@example.com은 같은 사람입니다. 광고 채널별로 +tag를 붙여 쓰는 팀이라면 이 한 줄이 중복의 절반을 없앱니다.
  2. 전화번호 정규화 — 숫자만 남깁니다. 하이픈·괄호·공백을 전부 제거하고, +82 또는 82로 시작하면 국가번호를 떼고 앞에 0을 붙입니다. +82 10 1234 5678, 010-1234-5678, (010) 1234 5678이 모두 01012345678이 됩니다.
  3. 병합 우선순위 — 정규화된 이메일이 있으면 그것을 식별자로 씁니다. 없으면 정규화된 전화번호를 씁니다. 둘 다 없으면 리드로 치지 않고 `needs_review`로 표시해 따로 뺍니다. 조용히 버리면 안 됩니다 — 폼이 고장 났을 때 알 방법이 사라집니다.
이 규칙은 완벽하지 않고, 완벽할 수도 없습니다. 같은 사람이 회사 이메일과 개인 이메일로 각각 문의하면 두 건으로 남습니다. 이건 자동으로 풀 수 없는 문제이므로, 그런 경우까지 잡으려 하지 말고 영업 담당자가 CRM에서 수동 병합할 수 있는 경로를 열어두는 것이 현실적입니다. 자동화의 목표는 100%가 아니라 사람이 봐야 할 양을 줄이는 것입니다.

필요한 것

  • n8n — 자체 호스팅 또는 Cloud. 폼과 광고 플랫폼이 웹훅을 쏴야 하므로 인터넷에서 접근 가능한 주소가 필요합니다.
  • Postgres 데이터베이스 하나 — 리드 테이블 하나를 만듭니다. 중복 방지를 데이터베이스 제약조건으로 거는 것이 이 설계의 핵심입니다.
  • Postgres 자격증명 — n8n → Credentials → Postgres. n8n이 Docker 안이고 DB가 호스트 머신이면 호스트에 host.docker.internal을 넣으세요.
  • Slack Bot User OAuth Tokenchat: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이 세 번 울립니다.

승인 게이트가 필요한 경우

리드가 들어오자마자 자동으로 메일이 나가는 구성은 위험할 수 있습니다. 실제 발주에서도 "내가 승인하기 전에는 아무것도 발송되지 않게 해달라"는 요구가 흔합니다. 자동 발송을 원하지 않는다면 이렇게 바꾸세요.

  1. Alert Only If New 노드 뒤에 발송 노드를 바로 잇지 말고, leads 테이블에 approved boolean DEFAULT false 컬럼을 추가합니다.
  2. Slack 알림 메시지에 담당자가 확인할 정보(회사·문의 내용)를 실어 보냅니다.
  3. 담당자가 확인 후 승인하면 approvedtrue로 바꾸고, 별도 스케줄 워크플로우가 승인된 건만 골라 발송합니다.
  4. 승인 절차 자체를 자동화하고 싶다면 사내 메신저 버튼으로 승인받는 방식이 있습니다 — 저희 telegram-approval 케이스가 그 구조를 다룹니다.

CRM 선택 — HubSpot이 정답은 아닙니다

이 워크플로우에는 CRM 연동 노드를 일부러 넣지 않았습니다. 시장에 따라 정답이 다르기 때문입니다.

  • 해외 B2B SaaS라면 HubSpot·Salesforce·Pipedrive가 자연스럽습니다. HTTP Request 노드 하나를 추가해 각 CRM의 컨택 생성 API를 호출하면 됩니다.
  • 국내 중소기업이라면 대부분 그런 CRM을 쓰지 않습니다. 실제로는 구글 시트, Notion, 채널톡, 또는 사내 시스템입니다. 안 쓰는 CRM에 맞춰 워크플로우를 짜면 그 워크플로우는 안 쓰입니다.
  • 아무것도 없다면 그대로 두세요. leads 테이블 자체가 이미 CRM의 최소 기능을 합니다. 조회는 SQL로, 공유는 시트 내보내기로 시작하고, 필요해진 시점에 CRM을 붙이면 됩니다. 처음부터 CRM을 정하려다 프로젝트가 멈추는 경우가 더 많습니다.
⬇︎ 워크플로우 다운로드 (lead-capture.json)
Postgres 자격증명 하나, Slack 자격증명 하나, 채널 이름만 넣으면 수집·정규화·중복병합·알림이 그대로 돕니다. CRM 연동은 각자 환경에 맞게 노드 하나를 추가하세요.
검증 범위를 정확히 밝힙니다. 정규화 규칙은 Node.js에서 12개 입력으로 실행해 확인했습니다 — 대소문자·공백 이메일, +태그 이메일, 하이픈/괄호/공백 전화, +8282 국가번호, 이메일·전화 동시 존재 시 우선순위, 그리고 식별 불가 세 경우(둘 다 없음 / @ 없는 문자열 / 너무 짧은 번호)가 needs_review로 분리되는 것까지 전부 통과했습니다. 병합 쿼리는 PostgreSQL 17에서 같은 리드를 세 번 넣어 행은 하나만 남고 `touch_count`가 1→2→3으로 증가하며 `is_new`가 첫 회에만 참임을 확인했습니다. 노드 타입·버전은 기존 배포 워크플로우와 동일한 조합입니다. 실제 Slack 게시와 실제 폼·광고 플랫폼 연동은 확인하지 않았습니다. 마지막 검증: 2026-08-15.

설정 순서 (20분)

  1. 테이블 생성 — 1단계 SQL을 DB에서 실행합니다.
  2. Postgres 자격증명 등록 — n8n → Credentials → PostgresTest로 초록불 확인.
  3. Slack 자격증명 등록 — api.slack.com/apps에서 앱 생성 → OAuth & Permissions → chat:write, channels:read 추가 → Install to Workspace → xoxb- 토큰을 n8n → Credentials → Slack API에 등록.
  4. 봇 초대 — 알림 받을 채널에서 /invite @봇이름.
  5. 워크플로우 import — 다운로드한 JSON을 n8n → Workflows → ...Import from File.
  6. 채널 이름 교체 + 자격증명 연결Notify Sales 노드의 REPLACE_WITH_YOUR_CHANNEL_NAME_OR_ID를 실제 채널명으로 바꾸고, 두 노드에 자격증명을 붙입니다.
  7. Activate — 켜야 Production 웹훅 주소가 살아납니다.
  8. 웹훅 주소 복사Lead Webhook 노드를 열어 Production URL을 복사합니다.
  9. 폼·광고 채널 연결 — 각 폼 빌더/광고 플랫폼의 웹훅 설정에 그 주소를 넣습니다. 보내는 필드 이름은 name, email, phone, company, message, source를 권장합니다. 이름이 다르면 Normalize 노드에서 매핑을 바꾸면 됩니다.
  10. 중복 테스트 — 같은 이메일로 폼을 두 번 제출해 보세요. 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만 올라갑니다.
1일 → 7분
리드 첫 응대 시간
100%
CRM 동기화율
수작업 0
취합 업무
20분
셋업 시간
리드가 들어오면 슬랙이 저보다 먼저 압니다. 식기 전에 전화 돌립니다.세일즈 리드

당신의 업무도
여기 들어갈 수 있어요.

가장 반복적인 업무 하나만 알려주세요. 자동화 시나리오를 그려드립니다.

리드 수집 자동화 — 중복을 CRM에 넣기 전에 걸러내는 법 — 케이스 스터디 — SynAct.ai