마케팅 데이터 분석

이벤트 이름과 속성을 관리하는 트래킹 명세

이벤트 이름·발생 조건·필수 속성·담당자를 하나의 명세로 관리합니다. GA4 규칙과 내부 계약을 구분하고, Python으로 문서와 검증 결과를 함께 생성하며 스키마 변경의 호환성을 확인합니다.

장바구니 보고서에는 add_to_cart가 있고 개발 로그에는 AddToCart가 있습니다. 구매 보고서의 value는 숫자인데 일부 이벤트에는 "100000"이라는 문자열이 들어옵니다. 이름을 맞추고 문자열을 숫자로 바꾸면 일단 집계는 됩니다. 그 뒤에도 결제 버튼 클릭이 구매로 기록돼 있었다면 전환율은 여전히 잘못 해석될 수 있습니다.

트래킹 명세는 이런 차이를 구현 전에 드러내는 문서입니다. 이벤트 이름, 발생 조건, 속성의 의미와 제약을 합의하고 실제 코드가 그 합의를 지키는지 확인할 수 있어야 합니다. 문서만 고치거나 코드만 고치면 어느 쪽을 기준으로 보고서를 읽을지 다시 물어야 합니다.

예제는 상품 조회·장바구니 추가·구매를 다루는 합성 커머스 명세입니다. 실제 고객 자료나 운영 로그를 사용하지 않으며, GA4에 데이터를 보내지 않습니다.

이벤트 이름보다 먼저 발생 조건을 정합니다

purchase라는 이름이 같아도 전송 시점이 다르면 서로 다른 행동을 세게 됩니다. 결제 요청을 보낸 순간, 결제 완료 응답을 받은 순간, 완료 화면을 다시 연 순간을 모두 구매로 기록하면 하나의 주문이 여러 번 나타날 수도 있습니다.

Google의 권장 이벤트에서 add_to_cart는 장바구니에 상품을 추가한 행동을, purchase는 상품을 구매한 행동을 뜻합니다. 내부 구현에서는 이 의미를 어떤 상태 변화로 확인할지 구체화해야 합니다. GA4 권장 이벤트 정의

합성 결제 흐름에서 purchase를 보내는 시점결제 버튼 클릭결제를 시도한 행동완료 여부는 아직 모릅니다→결제 처리 중성공·실패 미확정purchase 조건 미충족→백엔드 완료 확인주문·결제 상태 확인예제의 purchase 조건완료 화면 재조회 → 새 주문 발생으로 세지 않습니다.전송 재시도와 중복 제거는 주문 식별자를 기준으로 별도 설계합니다.업무 의미를 정한 도식 · 아래 형식 검증기만으로 이 흐름을 확인할 수는 없습니다.
이벤트의 발생 조건을 코드에서 관찰할 수 있는 상태와 연결합니다.

이 명세의 구매 생산자는 checkout_service로 정합니다. 백엔드에서 확인한 주문 상태를 기준으로 구매를 만들겠다는 내부 설계 선택입니다. 모든 GA4 구현에서 구매를 서버로 보내야 한다는 규칙은 아닙니다. 실제 전송 방식과 재시도 처리는 별도 구현이 필요하며, 브라우저에서 동일 구매를 함께 보내는 경우의 중복 처리도 확인해야 합니다.

담당자와 속성의 의미까지 한 표에 기록합니다

작은 명세도 마케터와 개발자가 같은 질문에 답할 수 있어야 합니다. 어떤 분석에 필요한 이벤트인지, 어느 서비스가 만들고 누가 변경을 검토하는지 함께 적습니다.

이벤트분석 목적발생 조건생산자 / 담당
view_item상품 상세 관심 행동상품 상세 정보가 화면에 정상 표시됐을 때web_storefront / storefront
add_to_cart장바구니 추가 행동추가 성공 후 이번에 추가된 상품·수량 기준web_storefront / cart
purchase결제 완료 구매 측정백엔드에서 결제 완료가 확인된 주문 기준checkout_service / checkout

add_to_cart의 items에는 이번에 추가된 상품과 수량을 넣습니다. 장바구니 전체를 넣는 것으로 바꾸면 반복 추가 시 같은 상품 금액이 다시 집계될 수 있습니다. 속성명이 같다는 이유만으로 의미도 같다고 볼 수 없습니다.

세 이벤트 모두 currency, value, items를 필수로 두고 구매에는 transaction_id를 추가합니다. coupon은 구매에서만 선택 속성으로 허용합니다. 상품 행에는 item_id, price, quantity가 모두 있어야 합니다. 이는 이 예제에서 채택한 내부 필수 조건입니다.

운영 명세에는 이 표 외에도 발생 시각의 기준, 식별자 범위, 중복 제거 키, 보존 기간과 민감정보 취급, 사용하는 리포트를 연결할 수 있습니다. 모든 이벤트에 같은 속성을 복제하기보다 분석 목적과 수집 필요성을 검토해 추가합니다.

GA4 규칙과 팀 내부 규칙을 구분합니다

GA4는 이벤트 이름의 대소문자를 구분하며 영어와 비영어 이름을 허용합니다. 이름은 문자로 시작하고 문자·숫자·밑줄을 사용하며 공백은 허용하지 않습니다. 예약 이름과 접두사도 별도로 확인해야 합니다. GA4 이벤트 이름 규칙

예제에서는 이름을 소문자 ASCII 문자로 시작하는 최대 40자의 snake_case로 제한합니다. snake_case는 여러 단어를 밑줄로 잇는 표기입니다. 내부 리뷰와 검색을 단순하게 하려는 선택이며, GA4가 영어 소문자만 받는다는 뜻은 아닙니다.

^[a-z][a-z0-9_]{0,39}$

이 정규식에 맞아도 명세에 없는 이벤트는 통과시키지 않습니다. 예를 들어 puchase는 문자 규칙에는 맞지만 등록한 purchase와 다른 이름입니다. 대소문자를 강제로 바꾸거나 오타를 추정해 고치면 어디서 잘못 만들었는지 숨길 수 있으므로, 검증기는 문제를 보고하고 원문을 유지합니다.

금액 속성에는 타입과 계산식을 함께 적습니다

value가 숫자라는 설명만으로는 부족합니다. 원인지 달러인지, 주문 전체인지 상품 합계인지, 배송비와 세금이 포함됐는지 정해야 비교할 수 있습니다. GA4 권장 구매 이벤트 문서는 value를 상품별 가격과 수량의 곱을 합한 금액으로 안내하고 배송비와 세금은 포함하지 않도록 설명합니다. value를 설정할 때의 currency 조건도 확인해야 합니다. GA4 purchase 속성

합성 명세는 KRW만 사용하며 상품 가격과 수량을 정수로 제한합니다. 가격은 0 이상, 수량은 1 이상이고 상품 목록은 비어 있으면 안 됩니다. 여기서 가격은 이 예제의 상품 금액 계산에 사용하는 단가입니다. 배송비·세금·환불·할인 배분은 입력에 넣지 않습니다. 실제 운영에서는 주문 원장의 금액 정의와 이 단가를 연결하는 규칙이 필요합니다.

속성내부 타입·범위의미
currency문자열, KRW만 허용예제 금액의 통화
value정수, 0 이상해당 이벤트 상품 금액 합계
items비어 있지 않은 배열해당 행동에 포함된 상품 행
items[].price정수, 0 이상상품 단가
items[].quantity정수, 1 이상해당 상품 수량
transaction_id공백만으로 이뤄지지 않은 문자열구매 주문 식별자

정수 KRW는 계산을 간단히 재현하기 위한 내부 제한입니다. 다중 통화나 소수 단가를 쓰는 서비스에는 통화별 정밀도와 반올림 규칙이 더 필요합니다. GA4가 정수 금액만 지원한다는 뜻도 아닙니다.

각 필드의 타입을 확인한 다음에는 필드 사이의 관계를 검사합니다.

value = Σ (items[i].price × items[i].quantity)

합성 구매 예시:
100,000 = 40,000 × 1 + 30,000 × 2

value가 99,000인 이벤트는 정수 검사에는 통과하지만 합계 검사에서 실패합니다. 타입 검사와 금액 대사는 서로 다른 오류를 찾습니다. 반대로 합계가 맞더라도 주문 원장에 그 구매가 존재한다는 사실까지 증명하지는 않습니다.

같은 명세에서 문서와 Python 검증기를 만듭니다

문서에 적힌 필수 속성과 검증기의 필수 속성을 따로 고치면 시간이 지나면서 어긋날 수 있습니다. 예제는 contract.json을 함께 읽도록 구성합니다. 사람이 읽는 표는 generate_plan.py가 만들고, analysis.py는 같은 파일의 규칙으로 이벤트를 검사합니다.

정의는 한 곳에서 고치고 문서와 검증에 함께 반영합니다contract.json이름 · 발생 조건 · 담당속성 규칙 · 금액 식 · 버전→→generate_plan.py → 문서 생성TRACKING_PLAN.md로 정의 검토analysis.py → 이벤트 검사타입 · 필수값 · 허용 속성 · 합계발생 조건은 문서에 남고, 실제 발생 여부는 통합 검증으로 확인합니다.자체 명세 형식 · 표준 JSON Schema 전체나 GA4 전체 규칙을 구현한 구조는 아닙니다.
담당자·목적·발생 조건은 사람이 검토하고, 표현 가능한 필드 제약은 코드로 검사합니다.

구매 이벤트의 정의는 다음과 같습니다. 전체 파일에는 각 속성의 타입·범위와 다른 두 이벤트의 정의도 들어 있습니다.

{
  "name": "purchase",
  "producer": "checkout_service",
  "owner": "checkout",
  "purpose": "결제 완료 구매 측정",
  "trigger": "백엔드에서 결제 완료가 확인된 주문 기준",
  "required": ["currency", "value", "items", "transaction_id"],
  "optional": ["coupon"]
}

검사 대상은 버전과 생산자 선언을 포함하는 내부 메시지입니다. properties 안에 분석용 속성을 넣습니다. 이 바깥 구조를 envelope라고 부르며, 실제 GA4 전송 요청의 구조를 그대로 나타내는 것은 아닙니다.

{
  "schema_version": 1,
  "event_name": "purchase",
  "producer": "checkout_service",
  "properties": {
    "currency": "KRW",
    "value": 100000,
    "transaction_id": "order-001",
    "items": [
      {"item_id": "sku-a", "price": 40000, "quantity": 1},
      {"item_id": "sku-b", "price": 30000, "quantity": 2}
    ]
  }
}

이 구조에서는 생산자 선언이 명세와 같은지 확인할 수 있습니다. 누구든 "producer": "checkout_service"라고 적을 수 있으므로 문자열 일치가 서비스의 진위를 인증하지는 않습니다. 실제 수집 경로에서는 인증과 권한 검사를 따로 둡니다.

합성 이벤트 11개에서 어떤 오류를 찾는가

정상 메시지 3개와 의도적으로 오류를 넣은 메시지 8개를 준비했습니다. 검증기는 다음과 같이 분류합니다.

사례결과확인한 문제
정상 조회·장바구니·구매통과 3개명세의 필드 제약과 합계에 부합
Purchase격리 후보내부 이름 규칙과 등록 이름 불일치
통화 누락격리 후보properties.currency 필수값 없음
빈 주문 ID격리 후보properties.transaction_id가 빈 문자열
수량 "2"격리 후보두 번째 상품의 수량이 문자열
구매 금액 99,000원격리 후보상품 합계 100,000원과 불일치
미등록 customer_email 속성격리 후보허용 속성 목록에 없음
명세 버전 2격리 후보현재 검사기는 버전 1만 처리
구매 생산자 web_storefront격리 후보명세에 선언한 생산자와 다름
합성 메시지 11개를 내부 명세로 검사한 결과accepted · 정상3개조회 · 장바구니 · 구매quarantine · 격리 후보8개이름 · 누락 · 빈 값 · 타입합계 · 미등록 속성 · 버전 · 생산자오류 경로와 코드를 남기며 메시지를 임의로 고치지 않습니다.오프라인 합성 예제 · 운영 시스템의 오류율이나 실제 전송 차단 건수가 아닙니다.
8개라는 결과는 설계한 오류 사례의 개수이며, 실제 서비스의 품질 추정치가 아닙니다.

customer_email의 값은 개인정보가 아닌 redacted-placeholder라는 합성 문자열입니다. 검증기는 그 내용을 이해한 것이 아니라 속성 이름이 명세 밖에 있다는 이유로 표시했습니다. 허용된 coupon 안에 민감한 값이 들어오는 경우까지 탐지하는 개인정보 검사기로 해석할 수는 없습니다.

오류 결과는 원문 값 대신 경로·코드·메시지를 반환합니다. 수량 오류라면 properties.items[1].quantity를 가리키므로 수정할 필드를 찾을 수 있습니다. 원문을 보관하거나 운영 로그에 남기는 정책은 이 오프라인 예제의 범위 밖입니다.

선택 속성을 추가할 때도 호환성을 확인합니다

기존 구매 명세에 선택 속성을 하나 추가하면 예전 메시지는 그 속성이 없어도 새 수신기에서 읽힐 수 있습니다. 반대 방향은 따로 확인해야 합니다. 기존 수신기가 모르는 속성을 거부한다면 새 속성이 포함된 메시지를 받지 못합니다.

허용하는 메시지의 집합을 A_old, A_new로 적으면 조건을 더 명확하게 볼 수 있습니다. 여기서는 버전 식별자가 라우팅된 뒤의 속성 규칙만 비교한다고 가정합니다.

기존 형식을 새 수신기가 모두 읽음: A_old ⊆ A_new
새 형식을 기존 수신기가 모두 읽음: A_new ⊆ A_old

선택 속성을 허용하면 메시지 집합이 커질 수 있으므로 첫 번째 조건은 만족해도 두 번째 조건은 실패할 수 있습니다. 두 조건을 구분하지 않고 “하위 호환”이라고만 적으면 배포 순서를 결정하기 어렵습니다.

예제는 버전이 다르면 먼저 거부하는 더 단순한 정책을 사용합니다. 버전 2로 바꾼 메시지는 내용이 같아도 버전 1 검사기를 통과하지 않습니다. 운영에서는 여러 버전을 처리하는 수신기를 먼저 배포하거나 변환 계층을 두는 등 실제 전환 경로를 정해야 합니다. 필드 이름을 그대로 둔 채 금액 의미를 바꾸는 변경도 분석에는 큰 영향을 줍니다. 타입 검사가 통과한다는 이유로 안전한 변경으로 분류해서는 안 됩니다.

직접 생성하고 검증하는 방법

트래킹 명세와 Python 검증 예제 ZIP을 풀고 marketing-metrics-11 폴더에서 실행합니다. Python 3.9 이상과 표준 라이브러리만 사용합니다.

python3 generate_plan.py
python3 analysis.py
python3 -m unittest

첫 명령은 TRACKING_PLAN.md를 다시 만듭니다. 두 번째 명령은 정상 3개와 격리 후보 8개, 각 사례의 오류 경로를 출력합니다. quarantine은 예제의 분류 이름이며 실제 전송을 차단하거나 별도 저장소로 이동시키는 기능은 구현하지 않았습니다.

테스트는 대소문자, 필수값, 숫자로 보이는 문자열과 불리언 수량, 빈 상품 목록, 상품 내부의 미등록 속성, 금액 합계, 선택 쿠폰, 버전·생산자 선언을 확인합니다. 입력을 임의로 고치지 않는지도 검사합니다. 이 코드는 학습용 자체 명세 형식이며 표준 JSON Schema나 GA4의 예약 이름·수집 한도 전체를 구현한 검증기가 아닙니다.

형식 검사 다음에는 실제 발생과 수신을 대조합니다

명세대로 생긴 메시지를 보냈다는 사실과 결제가 실제로 완료됐다는 사실은 다릅니다. 결제 실패에는 구매 이벤트가 없는지, 성공 시 주문 ID가 원장과 같은지, 새로고침과 재시도가 중복 구매를 만들지 않는지 실제 동작으로 확인해야 합니다.

웹의 gtag.js 구현에서는 DebugView로 수신된 이벤트와 이벤트·상품 단위 속성을 확인할 수 있습니다. Google도 전자상거래 수집 검증에 이 경로를 안내합니다. GA4 전자상거래 수집 검증

수신 확인 뒤에도 분석 테이블과 보고서에 기대한 의미로 반영됐는지 대조합니다. 명세의 발생 조건, 수집된 메시지, 최종 집계값을 한 주문으로 따라가면 어느 단계에서 의미가 달라졌는지 좁힐 수 있습니다. 문서를 고치는 작업도 그 경로의 검증 결과와 함께 남겨야 다음 변경을 검토할 수 있습니다.

이벤트 명세와 검증 결과를 대조하는 매도비

공식 문서는 2026년 9월 7일 확인했습니다. 명세와 메시지, 검사 결과는 설명용 합성 예제이며 실제 고객의 수집 환경을 검증한 결과가 아닙니다.

마케팅 데이터 모델·품질·지표 정의 연재 11편입니다. 이전 글: 관측 매출과 광고에 귀속된 매출을 나눠 보는 방법

🐹

자주 묻는 질문

GA4 이벤트 이름은 반드시 영어 소문자로만 써야 하나요?

GA4는 영어와 비영어 이름을 허용하며 대소문자를 구분합니다. 예제의 소문자 ASCII snake_case 규칙은 팀 내부의 선택입니다. GA4의 예약 이름과 접두사 등 실제 제약은 별도로 확인해야 합니다.

트래킹 명세에는 이벤트 이름 외에 무엇을 적어야 하나요?

분석 목적, 발생 조건, 전송 주체, 담당자, 필수·선택 속성, 타입·허용 범위, 금액의 단위와 계산식, 버전과 변경 절차를 기록합니다. 결제 버튼 클릭과 결제 완료처럼 의미가 다른 행동은 발생 조건에서 구분합니다.

스키마 검증을 통과하면 구매 데이터가 정확한가요?

형식·타입·선언한 계산식이 맞는다는 뜻입니다. 실제 주문의 존재, 결제 상태, 재전송 중복, 발신자 인증이나 개인정보 적합성까지 증명하지는 않습니다. 원장 대사와 실제 수집 경로의 검증이 추가로 필요합니다.

선택 속성을 추가하면 이전 버전과 항상 호환되나요?

항상 그렇지는 않습니다. 새 수신기가 기존 메시지를 읽는 것과 기존 수신기가 새 메시지를 읽는 것은 다른 조건입니다. 명세 밖 속성을 거부하는 기존 수신기는 새 선택 속성이 포함된 메시지를 거부할 수 있습니다.