본문으로 바로가기
리옵트 핸드북
리옵트 핸드북
GA4 고급 애널리틱스 설계

측정 아키텍처

측정 전략계정·속성·스트림 아키텍처데이터 수집 기반Consent·Privacy·PII 거버넌스

이벤트 설계

이벤트 분류 체계이벤트·파라미터 설계Key events & 퍼널SaaS 이벤트 플레이북Ecommerce & Revenue 이벤트

분석 활용

Reports·Explorations·AudiencesBigQuery Export 모델링QA·Debugging·Governance템플릿

부록

참고 자료검증 리포트업데이트 내역
핸드북›GA4 고급 애널리틱스›BigQuery Export 모델링

BigQuery Export 모델링

GA4 BigQuery Export의 dataset, table, event_params를 분석 모델로 풀어내는 방법

핵심 요약

  • BigQuery Export는 analytics_<property_id> dataset과 일별 events_YYYYMMDD table로 GA4 원시 이벤트를 적재합니다.
  • event_params, user_properties, items는 repeated field이라 UNNEST로 flatten해야 분석에 쓸 수 있습니다.
  • 표준 property는 daily batch export가 1일 100만 events로 제한되며, late arrival로 daily table은 며칠간 갱신될 수 있습니다.
  • 운영 mart는 daily table과 도착 지연을 기준으로 재계산합니다. 코호트는 가입 후 7일 관측창이 끝난 사용자만 분모에 넣습니다.
  • GA4 UI와 BigQuery는 attribution, export delay, identity, sampling/thresholding에서 차이가 나므로 그 차이를 명시적으로 기록합니다.

Export 완료 판정과 재계산 창

일별 표가 존재한다고 데이터가 완성된 것은 아닙니다. Google은 보통 표 날짜 이후 2일+당일의 늦게 도착한 데이터를 반영하며 예외적인 재처리는 더 늦게도 발생합니다. 최근 3일을 재계산하는 창과 마감 후 정정 절차를 나눕니다. 속성 시간대 변경은 export 누락·차이를 만들 수 있어 별도 변경으로 기록합니다.

Analytics 360은 Daily·Fresh Daily의 completeness signal을 Cloud Logging에서 확인할 수 있습니다. 신호는 export 처리일의 흐름을 나타내며 과거 이벤트 날짜의 파티션이 영구 불변임을 뜻하지 않습니다. Standard에 같은 신호가 있다고 가정하지 말고 표 갱신 시각·행 수·늦은 도착 비율을 점검합니다.

Streaming의 신규 사용자에는 traffic_source.name/source/medium이 없으므로 귀속 KPI는 daily에서 계산합니다. dbt 예제의 ga4_observed_through는 팀이 확정한 BigQuery TIMESTAMP 표현식 변수로, 검증하지 않은 사용자 문자열을 삽입하지 않습니다.

BigQuery Export · Export schema.

GA4 UI가 운영용 표면이라면, BigQuery Export는 원시 이벤트를 직접 소유하고 재가공하는 분석 기반입니다. Google 공식 문서에 따르면 GA4 property를 BigQuery에 연결할 때마다 analytics_<property_id> dataset과 일별 events_YYYYMMDD table이 생깁니다.

Export 구조

항목구조
Datasetanalytics_<property_id>
Daily tableevents_YYYYMMDD
Intraday tableevents_intraday_YYYYMMDD
Event row각 row가 하나의 event
Repeated fieldsevent_params, user_properties, items 등

Daily export는 전날 데이터를 한 번에 내보내고, streaming export는 당일 데이터를 계속 쌓아 올립니다. 공식 schema 문서에 따르면 late arrival 때문에 daily table은 이벤트 발생일 이후 며칠간 더 갱신되기도 합니다.

Standard property export 주의

항목기준
Daily export limit표준 GA4 property는 daily batch export에 1일 100만 events 제한
Streaming export이벤트 수 제한은 없지만 BigQuery streaming 비용 고려
BackfillAnalytics에서 BigQuery로 한 번 export한 데이터를 다시 export하지 않음
Filteringdata stream과 event exclusion으로 export volume 제어 가능

Event params flatten 예시

SELECT
  event_date,
  TIMESTAMP_MICROS(event_timestamp) AS event_ts,
  user_pseudo_id,
  event_name,
  (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'plan') AS plan,
  (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'activation_type') AS activation_type,
  (SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'trial_day') AS trial_day
FROM `my_project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20260501' AND '20260510'
  AND event_name = 'activation_complete';

SaaS activation cohort SQL

아래는 조회 기간 안의 첫 sign_up을 가입 시점으로 삼는 7×24시간 코호트입니다. 생애 최초 가입이 필요하면 별도의 사용자 차원에 최초 시점을 보존합니다. 속성 시간대를 Asia/Seoul로 가정하며 _TABLE_SUFFIX로 스캔 범위를 제한합니다.

DECLARE cohort_start DATE DEFAULT DATE '2026-10-01';
DECLARE cohort_end DATE DEFAULT DATE '2026-10-07';
DECLARE property_timezone STRING DEFAULT 'Asia/Seoul';
DECLARE observed_through TIMESTAMP DEFAULT TIMESTAMP '2026-10-18 00:00:00+09';

WITH source AS (
  SELECT
    user_pseudo_id,
    event_name,
    TIMESTAMP_MICROS(event_timestamp) AS event_ts
  FROM `my_project.analytics_123456789.events_*`
  WHERE _TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d', cohort_start)
    AND FORMAT_DATE('%Y%m%d', DATE(observed_through, property_timezone))
    AND user_pseudo_id IS NOT NULL
    AND event_name IN ('sign_up', 'activation_complete')
),
signups AS (
  SELECT user_pseudo_id, MIN(event_ts) AS signed_up_at
  FROM source
  WHERE event_name = 'sign_up'
    AND DATE(event_ts, property_timezone) BETWEEN cohort_start AND cohort_end
  GROUP BY user_pseudo_id
),
first_valid_activation AS (
  SELECT s.user_pseudo_id, MIN(e.event_ts) AS activated_at
  FROM signups s
  JOIN source e
    ON e.user_pseudo_id = s.user_pseudo_id
    AND e.event_name = 'activation_complete'
    AND e.event_ts >= s.signed_up_at
    AND e.event_ts <= TIMESTAMP_ADD(s.signed_up_at, INTERVAL 7 DAY)
  GROUP BY s.user_pseudo_id
)
SELECT
  DATE(s.signed_up_at, property_timezone) AS signup_date,
  COUNT(*) AS signups,
  COUNTIF(a.activated_at IS NOT NULL) AS activated_d7,
  SAFE_DIVIDE(COUNTIF(a.activated_at IS NOT NULL), COUNT(*)) AS activation_d7_rate
FROM signups s
LEFT JOIN first_valid_activation a USING (user_pseudo_id)
WHERE TIMESTAMP_ADD(s.signed_up_at, INTERVAL 7 DAY) <= observed_through
GROUP BY signup_date
ORDER BY signup_date;
경계 조건기대 결과
가입 전 활성화만 존재미전환
가입 전·가입 후 활성화 모두 존재가입 후 첫 유효 이벤트로 전환
정확히 가입+7일에 활성화전환 (이 예시의 우측 경계는 포함)
가입+7일 이후 활성화미전환
관측 기준일에 아직 7일 미경과분모에서 제외
consent 때문에 식별자 없음원본에는 보존하고 식별 가능한 코호트에서만 제외

7일을 “8개 달력 날짜”로 바꾸려면 날짜 기반 규칙으로 별도 정의합니다. 이 SQL은 UI의 모델링 사용자와 동일한 분모를 보장하지 않습니다. observed_through는 단순 현재 시간이 아니라 수집·export가 확인된 시각으로 정하고 late arrival 반영 뒤 재계산합니다.

모델링 레이어

Layer목적예시
RawGA4 export 원본events_YYYYMMDD
Stagingparams flatten, type castingstg_ga4_events
Intermediatesession/user/account 모델int_ga4_user_journey
MartKPI 보고용mart_activation_cohort, mart_revenue_funnel

dbt 모델 예시

models/staging/stg_ga4_events.sql

WITH source AS (
  SELECT *
  FROM {{ source('ga4_raw', 'events') }}
),
flattened AS (
  SELECT
    event_date,
    TIMESTAMP_MICROS(event_timestamp) AS event_ts,
    user_pseudo_id,
    user_id,
    event_name,
    (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'page_location') AS page_location,
    (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'source_surface') AS source_surface,
    (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'plan') AS plan,
    (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'activation_type') AS activation_type,
    (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'cta_location') AS cta_location,
    (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'cta_intent') AS cta_intent,
    (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'transaction_id') AS transaction_id,
    COALESCE(
      (SELECT value.double_value FROM UNNEST(event_params) WHERE key = 'value'),
      CAST((SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'value') AS FLOAT64)
    ) AS event_value,
    (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'currency') AS currency
  FROM source
)
SELECT *
FROM flattened

models/intermediate/int_ga4_funnel_events.sql

SELECT
  user_pseudo_id,
  user_id,
  DATE(event_ts, 'Asia/Seoul') AS event_day,
  MIN(IF(event_name = 'sign_up', event_ts, NULL)) AS signed_up_at,
  MIN(IF(event_name = 'activation_complete', event_ts, NULL)) AS activated_at,
  MIN(IF(event_name = 'begin_checkout', event_ts, NULL)) AS checkout_started_at,
  MIN(IF(event_name = 'purchase', event_ts, NULL)) AS purchased_at
FROM {{ ref('stg_ga4_events') }}
WHERE event_name IN ('sign_up', 'activation_complete', 'begin_checkout', 'purchase')
GROUP BY 1, 2, 3

models/marts/mart_activation_cohort.sql

WITH first_signup AS (
  SELECT
    user_pseudo_id,
    MIN(signed_up_at) AS signed_up_at
  FROM {{ ref('int_ga4_funnel_events') }}
  WHERE signed_up_at IS NOT NULL AND user_pseudo_id IS NOT NULL
  GROUP BY 1
),
first_activation AS (
  SELECT
    s.user_pseudo_id,
    MIN(e.event_ts) AS activated_at
  FROM first_signup s
  JOIN {{ ref('stg_ga4_events') }} e
    ON e.user_pseudo_id = s.user_pseudo_id
    AND e.event_name = 'activation_complete'
    AND e.event_ts >= s.signed_up_at
    AND e.event_ts <= TIMESTAMP_ADD(s.signed_up_at, INTERVAL 7 DAY)
  GROUP BY s.user_pseudo_id
)
SELECT
  DATE(s.signed_up_at, 'Asia/Seoul') AS signup_date,
  COUNT(*) AS signups,
  COUNTIF(a.activated_at IS NOT NULL) AS activated_d7,
  SAFE_DIVIDE(
    COUNTIF(a.activated_at IS NOT NULL),
    COUNT(*)
  ) AS activation_d7_rate
FROM first_signup s
LEFT JOIN first_activation a USING (user_pseudo_id)
WHERE TIMESTAMP_ADD(s.signed_up_at, INTERVAL 7 DAY) <= {{ var('ga4_observed_through') }}
GROUP BY 1

dbt_project.yml의 예제 관측 시점

아래 값은 팀이 확정한 SQL 표현식 예제입니다. 실제 적재가 확인된 시점으로 갱신하고 재집계합니다.

vars:
  ga4_observed_through: "TIMESTAMP('2026-10-18 00:00:00+09')"

models/schema.yml

version: 2

models:
  - name: stg_ga4_events
    columns:
      - name: event_name
        tests: [not_null]
      - name: event_ts
        tests: [not_null]
  - name: mart_activation_cohort
    columns:
      - name: signup_date
        tests: [not_null, unique]
      - name: activation_d7_rate
        tests:
          - dbt_utils.accepted_range:
              min_value: 0
              max_value: 1

dbt source는 stable daily table 기준

운영 mart는 events_intraday_YYYYMMDD가 아니라 stable daily export를 기준으로 만드는 편이 안전합니다. streaming export는 near realtime 확인용으로만 쓰고, user attribution과 완성 데이터가 필요한 KPI는 daily table에서 다시 계산하세요.

UI와 BigQuery 차이를 기록하기

차이처리
AttributionGA4 UI의 attribution model과 BigQuery raw event 기준을 구분
Export delaydaily table 완성 시점과 late arrival 반영
Consent modecookieless pings와 모델링 데이터 해석 주의
Identityuser_pseudo_id, user_id, account join key 차이
Sampling/thresholdingUI 탐색 제한과 raw export 집계 차이

출처

  • Set up BigQuery Export
  • BigQuery Export
  • BigQuery Export schema

관련 문서

검증 리포트

GA4 고급 애널리틱스 설계 핸드북의 구조·링크·근거 검증 결과

템플릿

GA4 이벤트 스펙, custom definition registry, key event checklist, QA checklist 템플릿

마케팅 애널리틱스 — 에이전트 대시보드

신규 브랜드 마케팅 전략 · GA4/Search Console 통합 분석, 어트리뷰션 모델 비교, 코호트/LTV 분석

On this page

Export 완료 판정과 재계산 창Export 구조Standard property export 주의Event params flatten 예시SaaS activation cohort SQL모델링 레이어dbt 모델 예시models/staging/stg_ga4_events.sqlmodels/intermediate/int_ga4_funnel_events.sqlmodels/marts/mart_activation_cohort.sqldbt_project.yml의 예제 관측 시점models/schema.ymlUI와 BigQuery 차이를 기록하기출처