전환 추적
광고를 클릭한 사람이 실제로 예약까지 했는지 기록하는 것을 전환 추적이라고 합니다.
전환 데이터가 없으면 광고 플랫폼은 "누가 예약했는지" 모릅니다. 그러면 광고를 아무에게나 보여주게 되고, 같은 예산으로 예약이 훨씬 적게 들어옵니다. 전환 데이터가 쌓일수록 "예약할 가능성 높은 사람"에게 광고가 자동으로 집중됩니다.
픽셀 코드 한 줄만 설치한 셋업은 2026년 기준 전환 데이터의 30~40%를 잃습니다. iOS 사파리 ITP, 광고 차단기, 3rd-party 쿠키 차단, 동의 거부 등이 원인입니다. Smart Bidding은 받는 신호가 부족해서 잘못된 사람에게 광고를 보여주고, ROAS는 드리프트합니다.
레이니는 동일한 전환 1건을 클라이언트와 서버 양쪽에서 전송하고, 같은 eventID로 자동 중복 제거하며, 가능한 모든 일급 데이터(first-party data)를 해싱하여 광고 플랫폼이 매칭에 쓸 수 있도록 합니다.
| 항목 | 일반 대행사 / 셀프 마케팅 | 레이니 |
|---|---|---|
| 픽셀 설치 | 클라이언트 픽셀 한 줄 | 클라이언트 픽셀 + 서버 CAPI 동시 |
| 이벤트 중복 제거 | 없음 (또는 추측) | UUID eventID 기반 자동 dedup |
| Enhanced Conversions | 미설정 | 자동 활성화 (해시 PII 8개 필드 자동 전송) |
| 전환 가치 | flat (또는 0) | Lead 35,000원 / Chat 5,000원 (테넌트별 커스터마이즈) |
| 전환 컨텍스트 | 알 수 없음 | source, 페이지명, 카테고리, 로케일, 레퍼런스 ID |
| iOS/Safari 전환 손실 | 30~40% 손실 (방치) | 서버 측 CAPI로 복구 |
| 광고 차단기 우회 | 픽셀 차단되면 끝 | 서버 측 경로는 영향 없음 |
| EMQ (Event Match Quality) | 모름, 측정 불가 | 6.4 → 7.2/10 (실측, 클리닉NB 기준) |
| Smart Bidding 신호 | 부족, 학습 더딤 | 풍부, 입찰 빠른 수렴 |
레이니가 추적하는 2가지 전환
| 전환 | 트리거 | Google Ads 카테고리 | Meta 이벤트명 | 기본 가치 |
|---|---|---|---|---|
| Lead | 예약 / 문의 폼 제출 | SUBMIT_LEAD_FORM (Primary) | Lead | 35,000 KRW |
| Chat | 채팅 위젯 열기 | ENGAGEMENT (Secondary) | Contact | 5,000 KRW |
Primary vs Secondary: Lead는 Google Ads 입찰 알고리즘이 학습하는 주요 전환입니다. Chat은 보고용 보조 전환으로 입찰에는 영향을 주지 않지만, 마케팅 깔때기의 상위 단계를 측정하는 데 사용됩니다.
어떻게 추적하나 — 클라이언트 측 전환
환자가 예약 폼을 제출하면 브라우저에서 즉시 3개의 신호가 발생합니다.
1. GA4 커스텀 이벤트
gtag('event', 'lny_lead', {
event_id: 'UUID',
tenant_id: 'clinicnb',
source: 'reservation_form',
reference_id: '367b5bf8-c698-4eed-b93c-285ec2610047'
});GA4에서 이 이벤트를 주요 이벤트(Key Event)로 설정하면 GA4 보고서에서 전환율, 잠재고객, 어트리뷰션 모델을 확인할 수 있습니다. tenant_id는 멀티테넌트 환경에서 조직별 필터링에 사용됩니다.
2. Google Ads 전환 태그
gtag('event', 'conversion', {
send_to: 'AW-1665670449/I9opCIuOnZQcEKLo3s1C',
value: 35000,
currency: 'KRW',
transaction_id: 'UUID' // 서버 측 전환과 중복 제거
});send_to의 형식은 AW-{customer_id}/{conversion_label}이며, 두 값 모두 자동 프로비저닝 시 생성·저장됩니다 (organizations.integrations.google_ads.customer_id + .conversions.lead.label). transaction_id는 서버 측 업로드의 orderId와 동일한 UUID이며, Google이 클라이언트/서버 전환을 자동으로 dedup합니다.
3. Meta Pixel Lead 이벤트 (with custom data)
fbq('track', 'Lead', {
content_name: '콜라겐터치 | 콜라겐닥터',
content_category: 'product/collagen-touch',
source: 'reservation_form',
locale: 'ko',
reference_id: '367b5bf8-c698-4eed-b93c-285ec2610047'
}, { eventID: 'UUID' });fbq('track', 'Lead', {})만 호출하면 Meta는 "어떤 페이지의 어떤 폼에서 발생한 lead인지" 모릅니다. 레이니는 content_name, content_category, source, locale, reference_id를 함께 전송하여 광고 관리자에서 캠페인별·페이지별·소스별 전환을 분리하여 볼 수 있게 합니다.
서버 측 전환
같은 eventID로 동시에 서버에 전송됩니다.
POST /api/track/conversion
{
"tenantId": "조직 UUID",
"type": "lead",
"eventId": "UUID (클라이언트와 동일)",
"eventSourceUrl": "https://contact.clinicnb.com/ko/product/collagen-touch",
"userData": {
"phone": "+821012345678",
"email": "user@example.com",
"displayName": "김민수",
"externalId": "367b5bf8-c698-4eed-b93c-285ec2610047",
"gclid": "...", "wbraid": "...", "gbraid": "...",
"fbp": "...", "fbc": "..."
},
"customData": {
"source": "reservation_form",
"contentName": "콜라겐터치 | 콜라겐닥터",
"contentCategory": "product/collagen-touch",
"locale": "ko",
"referenceId": "367b5bf8-c698-4eed-b93c-285ec2610047"
}
}라우트는 이 요청을 받으면 다음을 병렬로 수행합니다:
- Vercel 지오 헤더 추출:
x-vercel-ip-city,x-vercel-ip-country,x-vercel-ip-country-region,x-vercel-ip-postal-code,x-vercel-ip-latitude,x-vercel-ip-longitude,x-vercel-ip-timezone모두 자동 캡처 - 고객 행 지오 동기화:
update_customer_geoRPC로organization_customers행에 도시·지역·국가·우편번호·좌표·시간대 기록 - Meta CAPI 호출:
user_data로 Lead 이벤트 전송 - Google Ads API 호출:
uploadClickConversions로 Enhanced Conversions for Leads 전송 (gclid가 있을 때)
응답은 각 플랫폼별 결과를 명시적으로 반환합니다:
{
"ok": true,
"geo": { "city": "Gangnam-gu", "country": "kr", "region": "11", ... },
"customerSynced": { "ok": true },
"meta": { "ok": true, "status": 200, "eventsReceived": 1, "fbtraceId": "..." },
"googleAds": { "ok": true, "status": 200 }
}이벤트 중복 제거 (Deduplication)
클라이언트와 서버 양쪽에서 같은 전환을 보내면 2번 카운트될 수 있습니다. 레이니는 UUID 기반 dedup을 사용합니다.
trackConversion("lead") 호출 시:
1. eventId = crypto.randomUUID() ← 단일 UUID 생성
2. gtag('event', 'conversion', { transaction_id: eventId }) ← 클라이언트
3. fbq('track', 'Lead', { ... }, { eventID: eventId }) ← 클라이언트
4. POST /api/track/conversion { eventId, ... } ← 서버
├── Google Ads: orderId = eventId
└── Meta CAPI: event_id = eventId- Google Ads: 같은
transaction_id/orderId를 가진 전환을 자동 병합 - Meta: 같은
event_id를 가진 이벤트를 48시간 윈도우 내에 자동 병합
Meta 테스트 이벤트 탭에서 "중복 제거됨" 상태는 정상
서버 측 이벤트가 이미 처리된 브라우저 이벤트와 매칭되면 Meta는 해당 서버 이벤트를 "중복 제거됨(deduplicated)"으로 표시합니다. dedup이 정상 작동한다는 신호입니다. 둘 중 하나만 카운트되고, 둘 다 정상 수신되었음을 의미합니다.
고객 정보가 더 정확한 매칭을 만들어줍니다
예약 폼에서 받은 이름, 전화번호는 암호화(SHA-256 해시)되어 구글/메타로 전달됩니다. 원본 정보는 저장되지 않고 바로 암호화됩니다.
구글과 메타는 이 암호화된 정보를 자사 계정 정보와 대조해서 "광고를 본 사람이 맞다"는 걸 더 정확히 확인합니다. 이 과정을 매칭이라고 합니다. 매칭이 잘 될수록 광고 플랫폼이 "예약한 사람과 비슷한 사람"을 더 잘 찾아냅니다.
추가로 방문 위치 정보도 자동으로 수집됩니다. 서버에서 접속 IP를 기반으로 도시, 지역, 국가를 자동으로 파악합니다. 환자에게 "사시는 곳이 어디세요?"라고 물어볼 필요가 없습니다.
Meta CAPI user_data — 8개 해시 필드 + 4개 평문 필드
Meta CAPI가 받는 user_data는 다음 12개 필드입니다. 모든 PII는 SHA-256 해시되어 전송되며, 원본 값은 서버를 떠나지 않습니다.
| 필드 | 내용 | 해싱 | 정규화 |
|---|---|---|---|
em | 이메일 | SHA-256 | trim + lowercase |
ph | 전화 (E.164) | SHA-256 | digit-only, 국가코드 prefix (KR=82, JP=81 등), 선행 0 제거 |
fn | 이름 | SHA-256 | trim + lowercase. CJK 시장 특성상 fn/ln 분리 안 함, 전체 이름을 fn에 단일 저장 |
ct | 도시 | SHA-256 | lowercase, 공백·구두점 제거 (Vercel x-vercel-ip-city 자동) |
st | 지역 | SHA-256 | ISO 3166-2 코드 (Vercel x-vercel-ip-country-region) |
zp | 우편번호 | SHA-256 | lowercase, 공백·하이픈 제거 |
country | 국가 | SHA-256 | ISO 3166-1 alpha-2 lowercase |
external_id | 고객 DB UUID | SHA-256 | 안정적 외부 식별자, 같은 고객의 후속 이벤트(예약→방문→완료)와 매칭 |
fbp | Meta 브라우저 ID | 평문 | 쿠키에서 직접 |
fbc | Meta 클릭 ID | 평문 | 쿠키에서 직접 (fbclid가 자동 변환) |
client_ip_address | IP | 평문 | x-forwarded-for 첫 번째 |
client_user_agent | User-Agent | 평문 | 요청 헤더 |
왜 fn/ln을 분리하지 않는가 (한국 시장 특성)
영어권 도구는 보통 customer_name을 공백 기준으로 first_name / last_name으로 쪼갭니다. 한국·일본·중국 이름은 "김민수"를 통째로 First Name 필드에 입력하는 경우가 많아 잘못 분리하면 매칭률이 급락합니다. 레이니는 분리 없이 전체 이름을 fn에 단일 해시합니다. ln은 비웁니다. 잘못된 분리보다 빈 ln이 매칭률에 더 유리합니다.
전화번호 정규화 예시
입력: "010-1234-5678" + country=kr
처리: digit만 → "01012345678"
선행 0 제거 → "1012345678"
국가코드 prefix → "821012345678"
출력: SHA-256("821012345678")지원 국가코드: KR(82), JP(81), CN(86), US/CA(1), GB(44), HK(852), TW(886), SG(65), VN(84), TH(66), ID(62), MY(60), PH(63), IN(91), AU(61), DE(49), FR(33). 알 수 없는 국가는 입력값을 그대로 digit-strip만 합니다.
Custom data — 어떤 폼·페이지·로케일에서 발생했는지
user_data가 "누구인지"를 알려준다면, custom_data는 "무엇을 했는지"를 알려줍니다.
| 필드 | 값 예시 | Meta Ads Manager에서 활용 |
|---|---|---|
content_name | "콜라겐터치 | 콜라겐닥터" | 페이지 단위 전환 분리 보고 |
content_category | "product/collagen-touch" | 카테고리별 전환율 분석 (시술별, 부위별) |
source | reservation_form / customer_form / chat_popover | 예약 폼 vs 문의 폼 vs 채팅 분리 |
locale | ko / en / ja / zh | 다국어 캠페인 분리 |
reference_id | 고객 DB UUID | 캠페인 보고서에서 개별 전환 추적 |
value | 35000 | 광고 가치 기반 입찰 |
currency | "KRW" | 통화 설정 |
EMQ — Event Match Quality
Meta는 받은 user_data를 자사 사용자 프로필과 매칭하여 0~10점의 Event Match Quality(EMQ) 점수를 부여합니다. EMQ가 높을수록 Meta는 같은 사람을 더 정확히 식별하고, 광고 입찰 알고리즘이 더 빠르게 학습합니다.
| EMQ 점수 | 의미 |
|---|---|
| 0 ~ 3 | 거의 매칭 안 됨. 이벤트는 카운트되지만 어트리뷰션·최적화 효과 미미 |
| 4 ~ 5 | 부분 매칭. 일부 사용자만 광고 노출과 연결됨 |
| 6 ~ 7 | 양호. 대부분 사용자 매칭 |
| 8 ~ 10 | 우수. Smart Bidding 학습 효과 극대화 |
실제 사례 — 클리닉NB
| 시점 | EMQ | 보낸 필드 |
|---|---|---|
| Before (픽셀만) | 6.4 / 10 | fbp, fbc, ip, ua |
| After (CAPI + 자동 강화) | 7.2 / 10 | em, ph, fn, ct, st, zp, country, external_id, fbp, fbc, ip, ua + custom_data |
이 차이는 같은 광고 예산으로 약 15~30% 더 많은 전환을 어트리뷰션할 수 있게 해주며, 그만큼 Smart Bidding이 정확한 입찰을 합니다.
EMQ 8 이상으로 가려면 추가로 다음을 보낼 수 있습니다 (현재 미구현, 로드맵):
db(생년월일 YYYYMMDD): 예약 폼에서 받기 부담스러움 (전환율 저하)ge(성별 m/f): 한국 이름 기반 추론은 불안정, 직접 수집은 부담- 첫 방문 이후의 행동 시그널 (페이지뷰 횟수, 체류 시간, CTA 클릭)
Google Ads Enhanced Conversions for Leads
Google Ads의 Enhanced Conversions for Leads는 폼 제출자의 1st-party 데이터(이메일, 전화번호)를 SHA-256 해싱하여 Google Ads API로 직접 업로드하는 기능입니다. Google은 이 해시를 자사 사용자 프로필과 매칭하여 다음을 수행합니다.
- iOS / Safari ITP / 광고 차단기에서 잃어버린 전환을 복구합니다
- 크로스 디바이스 어트리뷰션을 지원합니다. 모바일에서 광고 클릭 후 데스크탑에서 폼 제출하는 케이스도 잡습니다
- Smart Bidding 학습 신호를 강화합니다
레이니에서는 이 기능이 자동으로 작동합니다.
Click conversion 페이로드 — 정확히 무엇이 가는가
uploadClickConversions API의 페이로드 구조:
{
"conversions": [{
"conversionAction": "customers/1665670449/conversionActions/7558612747",
"orderId": "fdc5c094-8ceb-420b-825f-2bac3db031bc",
"conversionDateTime": "2026-04-13 17:35:00+00:00",
"gclid": "Cj0KCQjw...",
"conversionValue": 35000,
"currencyCode": "KRW",
"userIdentifiers": [
{ "hashedEmail": "..." },
{ "hashedPhoneNumber": "..." }
]
}],
"partialFailure": true
}| 필드 | 출처 | 설명 |
|---|---|---|
conversionAction | integrations.google_ads.conversions.lead.action_id | 자동 프로비저닝 시 생성된 전환 액션 |
orderId | eventId (UUID) | 클라이언트 측 gtag transaction_id와 동일 → 자동 dedup |
gclid | URL ?gclid=...에서 1st-party 쿠키로 캡처 | Google 광고 클릭 식별자 |
wbraid / gbraid | iOS/Android 앱→웹 클릭 추적 ID | (해당하는 경우) |
conversionValue | integrations.google_ads.conversions.lead.value | 35,000 KRW |
currencyCode | KRW | 원화 |
userIdentifiers[].hashedEmail | 폼에서 감지된 이메일 → SHA-256 | Enhanced Conversions 핵심 |
userIdentifiers[].hashedPhoneNumber | 폼의 전화번호 → E.164 정규화 → SHA-256 | Enhanced Conversions 핵심 |
Click conversion이 받지 않는 것 — 알면 시간 절약되는 함정들
uploadClickConversions는 다음을 받지 않습니다 (실측 + Google API 문서 확인):
thirdPartyUserId(DB의 customer UUID): Click conversion API에서는 거부됩니다 ("The provided user identifiers are not supported. Use only hashed email or phone number"). Customer Match (uploadOfflineUserData) API에서만 유효합니다.addressInfo(도시·주·우편번호):hashedFirstName과hashedLastName을 함께 보내야 하는데, 한국 시장에서는 fn/ln 분리가 불안정하므로 보내지 않습니다. 잘못된 분리로 매칭률을 떨어뜨릴 위험이 있어 안 보내는 쪽이 낫습니다.- fn/ln 단독: CJK 시장에서는 단순 공백 기준 분리가 부정확하며, 잘못된 해시는 매칭에 해롭습니다.
레이니는 click conversion에 hashedEmail + hashedPhoneNumber + gclid만 보냅니다. Meta CAPI에는 fn, ct, st, zp, country, external_id까지 모두 보냅니다. 두 플랫폼의 페이로드 형태가 다른 이유입니다.
Customer Data Terms — MCC 1회 동의, 자동 retry 메커니즘
Google Ads의 Customer Data Terms는 Enhanced Conversions의 user_identifiers 업로드를 활성화하는 법적 합의입니다. 레이니는 모든 Google Ads API 호출에서 login-customer-id 헤더를 Plask MCC (Laney AI 464-669-1503)로 설정합니다. Google Ads는 이 약관 검사를 login-customer-id 계정에서 수행하기 때문에 MCC 1회 동의가 모든 child 클리닉에 영구 적용됩니다.
운영자가 약관에 동의하기 전이라도 기본 click conversion은 살아남도록 자동 retry 메커니즘이 있습니다 (packages/tracking/src/server/google-ads-conversion.ts):
// 1차 시도: user_identifiers 포함
const first = await sendToGoogleAds({ ...payload, userIdentifiers });
// 약관 미동의 에러 감지
if (first.text.includes("accepted_customer_data_terms") ||
first.text.includes("customer data processing terms")) {
console.warn("[tracking] Google Ads Enhanced Conversions terms not accepted — retrying without user identifiers");
// 2차 시도: user_identifiers 빼고 gclid만으로 재전송
const stripped = { ...payload };
delete stripped.userIdentifiers;
return sendToGoogleAds(stripped);
}결과:
- 약관 미동의 클리닉: gclid 기반 click conversion만 업로드 (Smart Bidding은 작동하지만 Enhanced 매칭률 향상은 없음)
- 약관 동의 클리닉: user_identifiers 포함 full Enhanced Conversions 작동 (retry 경로는 발화하지 않음)
- 콘솔 경고: 약관 미동의 클리닉의 라우트 응답에서
console.warn이 발생, Better Stack 로그에서 추적 가능
CRM에도 자동으로 기록됩니다
데이터 흐름 (단일 진실 원천)
같은 1건의 lead가:
- DB:
organization_customers한 행에 모든 필드가 모임 - Meta Ads Manager: user_data로 EMQ 7+
- Google Ads: Enhanced Conversions for Leads 매칭률 향상
- 대시보드 CRM: 사이드바에서 운영자가 도시·지역·시간대·고객 UUID를 즉시 확인
organization_customers 스키마 (강화 컬럼)
기존 컬럼 (display_name, email, phone, notes, tags, metadata, form_data)에 더해 20260413_customer_geo_columns 마이그레이션이 다음을 추가합니다:
| 컬럼 | 타입 | 출처 | 광고 플랫폼 매핑 |
|---|---|---|---|
city | text | x-vercel-ip-city | Meta ct |
region | text | x-vercel-ip-country-region (ISO 3166-2) | Meta st |
country | text | x-vercel-ip-country (ISO 3166-1 alpha-2 lowercase) | Meta country |
postal_code | text | x-vercel-ip-postal-code | Meta zp |
latitude | double precision | x-vercel-ip-latitude | (CRM 전용) |
longitude | double precision | x-vercel-ip-longitude | (CRM 전용) |
timezone | text | x-vercel-ip-timezone (IANA) | (CRM 전용) |
모두 nullable이며, 헤더가 없으면 그 컬럼만 비어있고 나머지는 정상 작동합니다.
update_customer_geo RPC (SECURITY DEFINER)
레이니는 서비스 롤 키 의존을 제거하기 위해 SECURITY DEFINER RPC를 사용합니다.
organization_customers의 RLS는 익명(anon) 역할의 직접 UPDATE를 차단합니다. 처음에는 SUPABASE_SERVICE_ROLE_KEY로 우회하려 했으나, 이 키가 만료/회전되면 트래킹이 조용히 멈춥니다. JWT는 형식상 유효(role: service_role, exp: 2036)였지만 Supabase가 서명을 거부하고, 라우트는 console.error만 남기고 사용자에게 200 OK를 반환한 일이 실제로 발생했습니다. 디버깅 시간 30분 손실.
해결: anon이 호출 가능한 SECURITY DEFINER 함수로 지오 컬럼만 변경할 수 있게 했습니다.
create or replace function update_customer_geo(
p_customer_id uuid,
p_city text default null,
p_region text default null,
p_country text default null,
p_postal_code text default null,
p_latitude double precision default null,
p_longitude double precision default null,
p_timezone text default null
) returns table (...)
language plpgsql
security definer
set search_path = public
as $$
begin
return query
update organization_customers c
set
city = coalesce(nullif(p_city, ''), c.city),
region = coalesce(nullif(p_region, ''), c.region),
country = coalesce(nullif(p_country, ''), c.country),
postal_code = coalesce(nullif(p_postal_code, ''), c.postal_code),
latitude = coalesce(p_latitude, c.latitude),
longitude = coalesce(p_longitude, c.longitude),
timezone = coalesce(nullif(p_timezone, ''), c.timezone),
updated_at = now()
where c.id = p_customer_id
returning ...;
end;
$$;
grant execute on function update_customer_geo(...) to anon, authenticated;핵심 설계:
coalesce(nullif(p_city, ''), c.city): 새 값이 있으면 덮어쓰고, 없으면 기존 값 유지. 라우트가 부분 페이로드를 보내도 안전security definer: anon이 호출하지만 함수 본문은 owner(postgres) 권한으로 실행됨grant execute … to anon: public landing 페이지에서 호출 가능
장점: Supabase 키 회전에 영향 없음, 수정 가능한 컬럼 화이트리스트, 호출 의도가 명시적, 감사 가능.
대시보드 사이드바
운영자가 신규 환자 카드를 열면:
[김민수]
이메일 minsu@example.com
전화 010-1234-5678
도시 Gangnam-gu ← 자동
지역 11 ← 자동 (서울특별시)
국가 kr ← 자동
우편번호 06236 ← 자동
시간대 Asia/Seoul ← 자동
메모 ...운영자에게 "고객님 사시는 곳이 어디세요?"라고 묻지 않아도 됩니다. 광고 플랫폼이 아는 정보 = CRM이 아는 정보 = 운영자가 보는 정보.
예약 1건의 광고 가치
레이니는 전환마다 금액 가치를 설정합니다.
| 전환 | 기본 설정 가치 | 의미 |
|---|---|---|
| 예약(Lead) | 35,000원 | 평균 예약 1건의 예상 가치 |
| 채팅(Chat) | 5,000원 | 채팅 상담 1건의 예상 가치 |
이 금액은 "예약 1건이 광고비 측면에서 얼마 가치냐"를 광고 플랫폼에 알려주는 수치입니다. 클리닉의 시술 단가에 따라 조정 가능합니다. 값은 integrations.google_ads.conversions.lead.value에서 변경 가능하며, 변경 즉시 새 전환부터 반영됩니다. 한방·치과·피부과·성형외과·도수치료 등 카테고리별로 다르게 설정하는 것이 권장됩니다.
가치가 설정되어 있어야 구글 광고가 Target ROAS 입찰 전략으로 "예약을 많이 만드는 방향으로" 입찰을 최적화할 수 있습니다.
전환 데이터가 광고 효율을 높이는 원리
전환 데이터가 구글/메타로 전달되면 광고 플랫폼의 AI가 학습합니다.
데이터가 쌓일수록 이 순환이 더 잘 돌아갑니다. 초기보다 3~6개월 후에 광고 효율이 더 좋아지는 이유입니다.
트러블슈팅 — 라우트 응답 해석
레이니의 /api/track/conversion은 모든 실패를 응답에 명시적으로 노출합니다.
라우트 응답 구조 (정상)
{
"ok": true,
"geo": {
"city": "Gangnam-gu",
"region": "11",
"country": "kr",
"postalCode": "060",
"latitude": 37.5245,
"longitude": 127.0354,
"timezone": "Asia/Seoul"
},
"customerSynced": { "ok": true },
"meta": {
"ok": true,
"status": 200,
"eventsReceived": 1,
"fbtraceId": "AzwjAcOAdQWvfZjC669ajd8"
},
"googleAds": { "ok": true, "status": 200 }
}meta 블록 에러 케이스
| 응답 | 원인 | 해결 |
|---|---|---|
{ "skipped": true, "reason": "no meta_pixel.pixel_id" } | 자동 프로비저닝 Meta Pixel 단계 실패 | 대시보드 > 설정 > 연동 확인 |
{ "skipped": true, "reason": "META_SYSTEM_USER_TOKEN unset" } | Vercel 환경변수 미설정 | vercel env add META_SYSTEM_USER_TOKEN production |
{ "ok": false, "status": 400, "error": "Invalid OAuth access token" } | 토큰 만료 | Meta Business Settings > System Users > Generate New Token. 새 토큰을 Vercel에 업데이트 후 재배포 |
{ "ok": false, "status": 0, "error": "fetch failed" } | 네트워크 문제 | 5분 후 재시도. https://metastatus.com 확인 |
{ "ok": true, "status": 200, "eventsReceived": 0 } | Meta가 silently 거부 | messages 필드 확인. Meta Events Manager > 진단 탭 |
googleAds 블록 에러 케이스
| 응답 | 원인 | 해결 |
|---|---|---|
{ "skipped": true, "reason": "no gclid/wbraid/gbraid" } | 정상 (오가닉 트래픽) | 없음 |
{ "skipped": true, "reason": "google_ads not configured" } | 자동 프로비저닝 Google Ads 단계 실패 | 대시보드 > 설정 > 연동 확인 |
{ "skipped": true, "reason": "google ads developer/manager env unset" } | Vercel 환경변수 누락 | GOOGLE_ADS_DEVELOPER_TOKEN, GOOGLE_ADS_MANAGER_ID, GOOGLE_ADS_CLIENT_ID, GOOGLE_ADS_CLIENT_SECRET, GOOGLE_ADS_REFRESH_TOKEN 5개 확인 |
{ "ok": false, "status": 200, "partialFailure": "customer data processing terms ..." } | 발생 안 함 (Plask MCC 이미 동의, 자동 retry가 user_identifiers 없이 재전송) | Plask 슈퍼유저에게 보고 |
{ "ok": false, "status": 200, "partialFailure": "gclid could not be decoded" } | 합성/유효하지 않은 gclid (테스트용 fake) | 실제 광고 클릭에서는 발생 안 함 |
{ "ok": false, "status": 200, "partialFailure": "Use only hashed email or phone number" } | userIdentifiers에 지원하지 않는 필드 포함 | 레이니 코드베이스에서는 발생 안 함 |
{ "ok": false, "status": 401 } | GOOGLE_ADS_REFRESH_TOKEN 만료 | Plask 슈퍼유저 계정으로 OAuth 흐름 다시 실행 |
customerSynced 블록 에러 케이스
| 응답 | 원인 | 해결 |
|---|---|---|
{ "ok": false, "reason": "no externalId" } | 정상 (채팅 open처럼 customer 행이 아직 없는 시점) | 없음 |
{ "ok": false, "reason": "customer not found for externalId" } | 잘못된 UUID 전달 | 호출자 코드 확인. 폼 제출 결과인 submission.id를 그대로 전달하는지 확인 |
{ "ok": false, "reason": "no geo fields" } | x-vercel-ip-* 헤더 모두 비어있음 (Vercel 환경 외부 호출) | 프로덕션에서는 정상. 로컬 개발 환경에서는 정상 동작 |
{ "ok": false, "reason": "Invalid API key" } (이론상) | Supabase JWT 시크릿 회전 | Supabase Dashboard > Settings > API에서 anon 키 새로 발급 후 Vercel NEXT_PUBLIC_SUPABASE_ANON_KEY 업데이트 |
Meta Events Manager 검증 절차
Overview 탭 — Lead 행 확인:
- 연결 방법 (Connection method):
브라우저 • 서버(양쪽이 다 들어오고 있다는 뜻) - EMQ: 6.0 이상이면 양호, 7.0 이상이면 우수
- 상태:
활성 (Active)
만약 브라우저만 보이면 서버 측 CAPI가 도달 안 함. 위 meta 블록 에러 케이스 확인.
Test Events 탭 — 개별 이벤트 검증:
| 상태 | 의미 |
|---|---|
| 처리됨 (Processed) | 이벤트 정상 수신 |
| 중복 제거됨 (Deduplicated) | 정상. dedup이 작동 중. 카운트는 1로 합쳐짐 |
| 거부됨 (Rejected) | 페이로드 오류. 메시지 확인 |
Google Ads Conversions 검증 절차
Tools → Conversions → Summary → Lead 전환 액션 클릭
- Source 컬럼:
Google tag (Web)(클라이언트) +Google Ads API(서버) 두 소스가 보여야 함 - Enhanced conversions 행: 활성 (Active)으로 표시되어야 함. Pending 또는 Disabled이면 약관 동의가 안 된 것
- 매칭률: 운영 시작 후 48시간 후 표시. 30~60%는 양호, 60% 이상은 우수
Vercel 환경변수 점검
cd $(mktemp -d)
vercel link --project laney-landing-org --yes
vercel env ls production | grep -E "META_SYS|GOOGLE_ADS|SUPABASE"필수 변수 (production): META_SYSTEM_USER_TOKEN, GOOGLE_ADS_CLIENT_ID, GOOGLE_ADS_CLIENT_SECRET, GOOGLE_ADS_REFRESH_TOKEN, GOOGLE_ADS_DEVELOPER_TOKEN, GOOGLE_ADS_MANAGER_ID, NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY
중요: env 변수를 변경하면 Vercel 재배포가 필요합니다. 런타임에서 자동 반영 안 됨.
일반적인 의심 상황 — 해결 흐름
"전환은 들어오는데 광고 비용이 효율이 안 나옴"
- EMQ 점수 확인 (Meta) / 매칭률 확인 (Google). 6 미만이면 enrichment 부족
- 실제 수신된 conversion 가치(KRW)가 적절한지 확인. 0 또는 너무 낮으면 Smart Bidding이 잘못 학습
- 광고 캠페인의 입찰 전략 확인. "Maximize conversions"보다 "Target ROAS"가 가치 기반 학습에 더 적합
"특정 광고에서 전환이 0인데 폼은 들어옴"
- URL의
gclid/fbclid가 캡처되는지 확인. 광고 링크에 자동 태깅이 활성화돼야 함 - 라우트 응답에서
googleAds.skipped: "no gclid"메시지 확인
"EMQ가 항상 6.4 근처에서 안 올라감"
customerSynced.ok: true인지 확인. 지오 필드가 채워지고 있어야 함- 페이지 도달 시 클라이언트가 Vercel을 거쳐 들어오는지 확인 (커스텀 도메인이 Vercel로 라우팅 되는지)
email을 보내고 있는지 확인. 폼에서 이메일을 받지 않으면 EMQ 상한이 7 정도- Meta가 매칭 데이터를 누적하기까지 24~72시간 필요. 새 트래킹 셋업 직후 즉시 8.0은 기대할 수 없습니다.
관련 문서
- 광고 트래킹 개요 — 광고 트래킹이 뭔지 처음 알아보기
- 자동으로 설정되는 것들 — 도메인 연결 후 자동 세팅 전체 목록
- 검색 엔진 연동 — 구글/네이버 검색 등록