GA4 + BigQuery로 ROAS 파이프라인 직접 만들기 — 플랫폼 대시보드 못 믿을 때
플랫폼 ROAS와 GA4 ROAS가 왜 다른지 정의부터 맞추고, GA4 event export에서 7일 last-touch ROAS를 재현하는 실행 가능한 BigQuery SQL을 정리합니다.
플랫폼·GA4·자체 집계의 ROAS가 다를 때, 정의를 문서화한 SQL로 비용과 구매를 다시 연결해야 차이의 원인을 설명할 수 있습니다.
왜 직접 만들어야 하나
플랫폼·도구별 ROAS의 한계:
| 출처 | 장점 | 한계 |
|---|---|---|
| Meta/Google Ads UI | 캠페인 분해와 운영이 빠름 | 매체별 attribution·전환 정의가 다름 |
| GA4 UI | 채널 통합 보고와 탐색이 쉬움 | reporting identity·모델링·보관 설정의 영향을 받음 |
| BigQuery event export | 이벤트 단위 쿼리와 정의 재현 | UI의 일부 모델링 값과 다를 수 있고 저장·쿼리 비용이 발생 |
자체 파이프라인의 진짜 가치는 “플랫폼이 안 보여주는 정의”를 자유롭게 만들 수 있다는 거예요.
- “브랜드 검색 제외 ROAS”
- “신규 vs 기존 분리 ROAS”
- “미귀속·매핑 누락을 함께 표시한 ROAS”
- “incrementality 보정된 ROAS”
1단계 — GA4 → BigQuery 익스포트 활성화
GA4 속성 설정 → BigQuery 연결 → “매일 익스포트” 옵션을 켭니다. 연결 이후 다음 형식의 일별 테이블이 생성됩니다. Standard 속성의 일별 export에는 하루 100만 이벤트 한도가 있고, 기존 GA4 데이터는 자동으로 소급 적재되지 않습니다.
project.analytics_<property_id>.events_YYYYMMDD각 row가 한 이벤트(page_view, purchase, click 등). 가장 자주 쓰는 컬럼들:
| 컬럼 | 타입 | 의미 |
|---|---|---|
event_date | STRING | YYYYMMDD 형식 |
event_timestamp | INT64 | 마이크로초 단위 |
event_name | STRING | page_view, purchase 등 |
event_params | REPEATED | key-value 배열 (URL, source, campaign 등) |
ecommerce | RECORD | purchase일 때 transaction_id, purchase_revenue |
traffic_source | RECORD | 사용자의 최초 획득 source, medium, name |
device | RECORD | category, os, mobile_brand_name |
geo | RECORD | country, region, city |
user_pseudo_id | STRING | 클라이언트 ID (사용자 식별 핵심) |
스트리밍 export를 함께 켜면 events_intraday_YYYYMMDD가 생기지만 best-effort 데이터라 누락 가능성이 있습니다. 일별 테이블이 완성되면 intraday 테이블은 삭제되고, 지연 이벤트 때문에 일별 테이블은 최대 72시간 갱신될 수 있습니다. 운영 대시보드에는 “진행 중”과 “확정” 상태를 나눠 표시해야 하는 이유입니다.
가장 헷갈리는 게 event_params예요. 배열이라 SQL에서 펼쳐야(UNNEST) 합니다. “4월 구매 이벤트의 utm_source 분포는?” 같은 단순 질문도 결국 UNNEST 한 번 거쳐야 답이 나와요.
2단계 — 광고비 + 매출 일별 join
광고비는 별도로 적재해야 합니다. Google Ads는 BigQuery Data Transfer Service를 쓸 수 있고, 다른 채널은 API 응답을 ads_spend(date_kst, channel, campaign_id, spend_krw) 계약으로 정규화합니다. 순매출은 GA4의 속성 통화값을 원화로 간주하지 않고 transaction_id가 유일한 주문 DB의 orders(transaction_id, net_revenue_krw)를 사용합니다. UTM과 매체 campaign ID를 잇는 dim_paid_campaign_map도 준비합니다.
아래 쿼리는 7일 안의 마지막 수집 paid-campaign touch를 구매에 하나만 붙이고, 매출을 touch 날짜로 되돌린 뒤 같은 날짜의 광고비와 결합합니다. 플랫폼 attribution을 복제하는 SQL이 아니라 조직이 명시적으로 선택한 custom last-touch 정의입니다.
WITH google_touches AS ( SELECT e.user_pseudo_id, TIMESTAMP_MICROS(e.event_timestamp) AS touch_time, 'google_ads' AS channel, CAST( e.session_traffic_source_last_click.google_ads_campaign.campaign_id AS STRING ) AS campaign_id, 1 AS mapping_priority FROM `project.analytics_<id>.events_*` e WHERE _TABLE_SUFFIX BETWEEN '20260325' AND '20260430' AND e.event_name = 'session_start' AND e.user_pseudo_id IS NOT NULL AND e.session_traffic_source_last_click.google_ads_campaign.campaign_id IS NOT NULL),manual_touches AS ( SELECT e.user_pseudo_id, TIMESTAMP_MICROS(e.event_timestamp) AS touch_time, m.channel, m.campaign_id, 2 AS mapping_priority FROM `project.analytics_<id>.events_*` e JOIN `project.mart.dim_paid_campaign_map` m ON LOWER(e.collected_traffic_source.manual_source) = LOWER(m.utm_source) AND LOWER(e.collected_traffic_source.manual_medium) = LOWER(m.utm_medium) AND LOWER(e.collected_traffic_source.manual_campaign_name) = LOWER(m.utm_campaign) WHERE _TABLE_SUFFIX BETWEEN '20260325' AND '20260430' AND e.event_name = 'session_start' AND e.user_pseudo_id IS NOT NULL),touches AS ( SELECT * EXCEPT(mapping_priority) FROM ( SELECT * FROM google_touches UNION ALL SELECT * FROM manual_touches ) QUALIFY ROW_NUMBER() OVER ( PARTITION BY user_pseudo_id, touch_time ORDER BY mapping_priority ) = 1),purchases AS ( SELECT e.ecommerce.transaction_id, e.user_pseudo_id, TIMESTAMP_MICROS(e.event_timestamp) AS purchase_time, o.net_revenue_krw AS revenue_krw, e.event_timestamp FROM `project.analytics_<id>.events_*` e JOIN `project.mart.orders` o ON e.ecommerce.transaction_id = o.transaction_id WHERE e.event_name = 'purchase' AND e.ecommerce.transaction_id IS NOT NULL AND _TABLE_SUFFIX BETWEEN '20260401' AND '20260430' QUALIFY ROW_NUMBER() OVER ( PARTITION BY e.ecommerce.transaction_id ORDER BY e.event_timestamp ) = 1),attributed AS ( SELECT p.*, t.touch_time, t.channel, t.campaign_id FROM purchases p LEFT JOIN touches t ON p.user_pseudo_id = t.user_pseudo_id AND t.touch_time BETWEEN TIMESTAMP_SUB(p.purchase_time, INTERVAL 7 DAY) AND p.purchase_time QUALIFY ROW_NUMBER() OVER ( PARTITION BY p.transaction_id ORDER BY t.touch_time DESC NULLS LAST ) = 1),revenue_daily AS ( SELECT DATE(touch_time, 'Asia/Seoul') AS date_kst, channel, campaign_id, COUNT(*) AS orders, SUM(revenue_krw) AS revenue_krw FROM attributed WHERE touch_time IS NOT NULL GROUP BY 1, 2, 3),spend_daily AS ( SELECT date_kst, channel, campaign_id, SUM(spend_krw) AS spend_krw FROM `project.mart.ads_spend` GROUP BY 1, 2, 3)SELECT COALESCE(r.date_kst, s.date_kst) AS date_kst, COALESCE(r.channel, s.channel) AS channel, COALESCE(r.campaign_id, s.campaign_id) AS campaign_id, COALESCE(s.spend_krw, 0) AS spend_krw, COALESCE(r.revenue_krw, 0) AS revenue_krw, COALESCE(r.orders, 0) AS orders, SAFE_DIVIDE( COALESCE(r.revenue_krw, 0), COALESCE(s.spend_krw, 0) ) AS roasFROM revenue_daily rFULL OUTER JOIN spend_daily s ON r.date_kst = s.date_kst AND r.channel = s.channel AND r.campaign_id = s.campaign_idGoogle Ads 자동 태깅은 세션의 google_ads_campaign.campaign_id로, 수동 UTM은 매핑 테이블로 연결합니다. 과거 export에 세션 필드가 없는지는 샘플 날짜의 null 비율로 먼저 확인합니다. 이 touch는 광고 클릭 로그가 아니라 수집 campaign이므로 결과명도 “7일 last collected paid-campaign touch”가 정확합니다. 미귀속 구매 비율과 주문 DB의 transaction_id 합계를 감시해야 하며, user_pseudo_id가 바뀌는 cross-device 구매는 복구되지 않습니다.
이 API로 실제 무엇을 만들 수 있는지
매일 새벽 BigQuery scheduled query로 결과를 만들면 마케터가 SQL을 몰라도 쓰는 대시보드 테이블이 됩니다. 현재 SQL의 날짜·채널·campaign ID·원화 지표에 후속 enrichment를 더한 운영 스키마 예시는 다음과 같습니다.
| 컬럼 | 의미 |
|---|---|
date_kst | KST 집계 일자 |
channel | google_ads / meta / tiktok / naver / other |
campaign_name | 캠페인 |
is_new_user | 신규 vs 기존 분리 진단 |
device_category | mobile / desktop / tablet |
spend_krw, revenue_krw, orders | 원화 비용·순매출·주문 수 |
roas | 합의한 attribution 정의의 매출/광고비 |
status_flag | unattributed / mapping_missing / provisional / ok |
이 테이블을 Looker Studio·Tableau·내부 BI로 연결하면, 마케터는 채널·캠페인 ROAS와 데이터 품질 상태를 같은 정의로 봅니다. 여기에 다음 운영 도구를 얹을 수 있습니다.
- 일별 비용 적재 누락과 GA4 지연 데이터를 구분하는 파이프라인 상태판
- 플랫폼 보고값·GA4 이벤트·주문 DB를 나란히 보여주는 reconciliation 리포트
- 신규/기존 고객, 브랜드/논브랜드, 환불 차감 여부를 전환할 수 있는 ROAS 뷰
- 미매핑 UTM, 중복
transaction_id, 비정상 환율을 배포 전에 잡는 데이터 품질 경보
잘 빠지는 함정 — 직접 만들 때만 발생하는 것들
1) 전환일과 광고 상호작용일 혼동
사용자가 4월 30일에 클릭하고 5월 1일에 결제하면 구매 이벤트는 5월 1일에 생기지만 광고 상호작용은 4월 30일에 있습니다. “전환 발생일” 보고와 “상호작용일에 귀속한” 보고는 서로 다른 질문입니다. 임의의 7일 offset으로 날짜를 맞추지 말고, 조직이 정한 lookback window 안에서 touchpoint와 conversion을 timestamp로 매칭한 뒤 두 보고 날짜를 모두 보존합니다.
2) Cross-device 이슈
같은 사람이 모바일에서 클릭하고 데스크탑에서 결제하면 user_pseudo_id가 달라져 별개 사용자로 잡힙니다. GA4 user_id(로그인 ID) 매칭을 켜뒀으면 일부 회복.
3) 환불·캐시백 빠뜨리기
GA4 purchase만 수집하면 이후 환불을 자동으로 알 수 없습니다. transaction_id를 포함한 refund 이벤트를 구현했거나 주문 DB에 환불 상태가 있을 때만 구매와 연결해 차감할 수 있습니다. 부분 환불·쿠폰·부가세·배송비 포함 규칙도 매출 계약에 명시합니다.
운영 팁 — 마케터 데이터 팀이 자주 묻는 것
_TABLE_SUFFIX BETWEEN으로 읽는 날짜를 제한하고 scheduled query 결과만 BI에 연결합니다. export 한도와 스트리밍 누락 가능성은 상태판에 함께 표시합니다.- 1차 모델은 문서화한 last-touch와 정합성 검사부터 만듭니다. 주문 DB 대조를 통과한 뒤 멀티터치나 실험 보정을 별도 모델로 추가합니다.
- attribution 범위·환불 차감·채널 매핑표를 한 페이지 데이터 계약으로 남기고 분기마다 갱신합니다. A/B·geo-lift 결과도 같은 주문·비용 계약을 재사용합니다.
마치며
플랫폼 대시보드 ROAS는 각 플랫폼의 attribution·보고 시간·전환 설정에 따라 계산된 숫자입니다. GA4 event export + BigQuery + SQL로 자체 파이프라인을 만들면 그 숫자를 무조건 대체하는 것이 아니라, 차이를 설명하고 조직의 의사결정 정의를 재현할 수 있습니다.
가장 먼저 만들 뷰는 화려한 멀티터치 모델보다 date × channel × campaign 단위의 비용·주문·환불·매핑 상태표가 좋습니다. 이 추천은 실제 회사 성과를 인용한 것이 아니라, 데이터 계약을 작은 단위에서 검증하기 위한 구현 순서입니다.
참고
- GA4 BigQuery Export 공식 문서 — 익스포트 방식·한도·지연 데이터
- BigQuery — events_* 테이블 스키마 — 모든 컬럼 정의
- GA4 dimensions and metrics — UNNEST 패턴 — event_params 펼치기
- Looker Studio + BigQuery 연결 — BI 시각화
- The Data Warehouse Toolkit — Kimball — 마케팅 데이터 모델링 표준서
Analytics Ops (GA4·GTM) 카테고리의 다른 글
전체 보기 →-
2026·06·18
GA4 컨설팅 체크리스트 30 — 설치·권한·이벤트·전환·attribution을 한 번에 점검하기
새 GA4 계정을 넘겨받았거나 컨설팅을 시작할 때, 무엇부터 확인해야 하나. 설치 정합성부터 권한, 이벤트 택소노미, 전환, attribution 설정까지 30개 점검 항목을 영역별로 정리했습니다. 나가서 바로 쓰는 체크리스트.
-
2026·05·16
광고 SQL·BI 안티패턴 7가지 — ROAS 보고서를 거짓말로 만드는 SQL 함정
광고 데이터를 SQL로 집계할 때 반복적으로 깨지는 7가지 패턴 — 중복 조인·attribution window 누락·시간대 미스·conversion lag·환율·채널 매핑·dedup. 마케터·BI팀이 실무에서 만나는 함정을 실제 SQL 반례와 함께 정리합니다.
-
2026·05·09
Marketing analytics maturity model — last-click부터 triangulation까지 5단계
마케팅 측정의 성숙도는 5단계로 나뉩니다. last-click → multi-touch → MMM → lift study → triangulation. 우리 팀이 어디 있는지 진단하고 다음 단계 로드맵을 잡는 한 가지 모델.
-
2026·05·08
Server-side Tagging과 Conversion API — 1st-party 데이터를 직접 운영하는 법
브라우저에서 보내는 픽셀이 30~50% 차단되는 시대에, 서버에서 광고 플랫폼으로 직접 이벤트를 보내는 server-side tagging과 Meta CAPI·GA4 Measurement Protocol·TikTok Events API의 핵심을 마케터 시선에서 정리합니다.