광고 노출 이벤트를 세 플랫폼에서 같은 뜻으로 세는 규약
광고 분석에서 ad_impression이라는 이름은 플랫폼마다 다른 순간에 기록되기 쉬워요. iOS에서는 광고 로딩이 끝났을 때, Android에서는 SDK가 impression을 알렸을 때, Web에서는 광고 요소를 DOM에 붙였을 때 같은 이름으로 보내는 경우예요. 이렇게 모은 숫자는 한 표에 들어가지만 세는 기준은 서로 달라요. 플랫폼에 따라 실제보다 많이 세기도 하고 적게 세기도 해요.
한 이름 아래 섞이는 것은 다섯 가지예요. 광고를 내려받은 것, 화면 계층에 붙인 것, viewport와 겹친 것, SDK가 노출로 집계한 것, 수익이 통지된 것이에요. 여기에 자동으로 기록되는 이벤트 위에 수동 기록을 더하면 같은 노출이 두 번 세어져요.
이 글은 Google, Firebase, Web 문서에서 callback의 의미를 비교해 정리한 규약이에요. 문서는 2026년 10월 4일에 다시 열어 확인했어요. SDK 버전, 측정한 로그, 실행 결과는 이 글에 싣지 않았어요. 어떤 광고 공급자를 고를지는 다루지 않아요.
로딩 완료와 DOM 부착은 노출이 아니에요
광고를 다 받았다고 해서 사용자가 본 것은 아니에요. 로딩이 끝난 뒤에도 광고가 화면에 붙지 않을 수 있고, 그 사이에 화면이 닫히거나 슬롯이 다른 광고로 바뀔 수 있어요. 준비가 끝났다는 사실만으로는 impression을 인정할 수 없어요.
Web에서 DOM에 붙인 것도 본 증거로는 모자라요. IntersectionObserver는 대상과 root가 겹치는 비율이 threshold를 넘나드는지를 관측해요. threshold의 기본값은 0이라서 경계가 닿기만 해도 통지가 올 수 있어요. observe()를 시작한 직후에는 보이지 않는 대상에 대해서도 첫 통지가 올 수 있어요. 이 통지를 광고 네트워크가 과금하는 근거로 볼 수는 없어요. 문서가 설명하는 것은 겹침의 관측까지예요.
광고가 거치는 여섯 단계
섞여 있던 신호를 단계로 나누고, 단계마다 아직 정해지지 않은 것을 함께 적었어요.
requested: 요청을 시작한 단계예요. 응답, 표시, 노출은 모두 정해지지 않았어요.loaded: 응답이나 creative를 받은 단계예요. 표시와 impression은 정해지지 않았어요.rendered: view나 DOM에 붙인 단계예요. 실제로 보이는지와 SDK가 집계했는지는 정해지지 않았어요.app_visible: 앱이 정한 viewport 조건을 채운 단계예요. 네트워크가 과금하는지는 정해지지 않았어요.sdk_impression: SDK가 노출로 집계한 단계예요. 수익 정보가 오는지와 그 정밀도는 정해지지 않았어요.paid_value: 노출별 매출이 관측된 단계예요. 정산 금액과 정확성은 정해지지 않았어요.
플랫폼의 신호는 이 단계에 이렇게 대응해요.
- iOS의
bannerViewDidReceiveAd(_:)와 Android의onAdLoaded()는loaded예요. impression으로 세지 않아요. - iOS의
bannerViewDidRecordImpression(_:)와 Android의onAdImpression()은sdk_impression이에요. 자동 수집이 없을 때만 앱이 기록해요. - Web에서 광고 요소를 mount한 것은
rendered예요. impression으로 세지 않아요. - observer 규칙을 채운 것은
app_visible이에요. 인하우스 광고에만 써요. - iOS의
paidEventHandler와 Android의OnPaidEventListener는paid_value예요. 건수를 더하지 않아요.
무엇을 노출 한 건으로 셀지
impression의 근거로는 SDK의 통지를 가장 먼저 써요. SDK가 없는 인하우스 광고는 가시성 조건을 글로 정해 두고, 그 조건을 채웠을 때 evidence=app_visible로 기록해요. 이 집단은 SDK가 관측한 집단과 따로 분석해요. 같은 이벤트 이름을 쓴다고 해서 두 집단을 그대로 비교할 수 있는 것은 아니에요.
이 규약에서 paid 통지는 이미 센 impression에 붙는 수익 정보로 다뤄요. 건수를 하나 더하지 않아요. 통화와 precision은 그대로 보존해요. precision은 unknown이나 estimated일 수 있어서, 이 값을 최종 정산으로 읽으면 안 돼요. 수익이 제공되지 않았을 때 0으로 채우거나 건수에서 거꾸로 계산하지도 않아요. Firebase의 ad_impression에 value를 넣는다면 currency도 함께 넣어야 하고, precision은 별도의 매출 경로에 보관해요.
실패 쪽도 범위를 좁혔어요. ad_load_failed는 실제로 요청을 보낸 공급자가 응답에 실패한 경우에만 기록해요. 등록되지 않았거나 꺼져 있어서 건너뛴 공급자는 실패가 아니에요. 같은 이유로, 공급자를 골랐다는 것만으로는 ad_impression을 보내지 않아요. 화면이 닫히거나 unmount된 뒤에 취소된 요청의 통지가 늦게 도착해도 현재 슬롯의 결과로 치지 않아요.
자동 기록이 있으면 앱은 보내지 않아요
Firebase와 연동한 AdMob에서 자동 기록이 켜져 있으면 앱이 같은 이벤트를 수동으로 보내면 안 돼요. onAdImpression() 안에서 같은 GA4 기록을 한 번 더 보내면 노출 한 번이 두 건으로 집계돼요. 자동 수집이 없는 SDK는 callback 한 번에 한 건을, 인하우스 광고는 가시성 조건을 한 번 채울 때 한 건을 기록해요. DOM만 관측할 수 있는 외부 Web 콘텐츠는 근거 규약을 따로 정해야 해요.
자동 기록에는 slot이나 selection_source 같은 필드가 빠질 수 있어요. 빠진 필드를 채우려고 impression을 하나 더 보내지 않고, 누락을 받아들인 채 evidence별로 분석해요. paid 데이터를 별도 서버로 보낼 때에도 GA4 이벤트로 바꾸기 전에 자동 기록이 켜져 있는지 먼저 확인해요. 자동 기록과 수동 기록이 채우는 필드의 차이도 규약에 적어 둬요.
슬롯, attempt, 광고 인스턴스를 구분해요
중복을 없애려면 무엇을 한 건의 단위로 볼지 정해야 해요. 슬롯은 UI가 살아 있는 동안의 단위이고, attempt는 공급자 하나에 보낸 요청 한 번이고, 광고 인스턴스는 creative 하나가 살아 있는 동안의 단위예요.
슬롯 하나에서 요청이 실패한 뒤 다시 요청해 성공하고, refresh로 두 번째 노출이 생기는 경우를 그린 도식이에요.
slot instance
├─ attempt 1 → load failed
└─ attempt 2 → loaded → impression 1
└─ refresh → impression 2
세션에 Boolean 하나만 두면 refresh로 생긴 노출을 놓쳐요. 반대로 callback을 전부 보내면 재시도나 재마운트 때 중복이 생길 수 있어요. 그래서 실패는 attempt마다, impression은 인스턴스마다 한 번만 남겨요. refresh로 새 creative가 오면 새 인스턴스로 보고, 같은 인스턴스가 다시 통지한 것만 빼요. 이 구분에는 메모리 안의 식별자면 충분해요. GA4에 고유 ID를 보내서 값의 종류가 지나치게 많은 차원을 만들 필요는 없어요.
앱 안에서 주고받는 이벤트의 모양
앱 내부에서는 필드 수를 적게 두고 값도 정해진 것만 써요. 아래는 GA4의 표준 스키마가 아니고 앱 내부에서 쓰는 예시예요.
{
"name": "ad_impression",
"provider": "global_network",
"format": "banner",
"slot": "bottom",
"evidence": "sdk_impression",
"selection_source": "normal"
}
provider, format, slot은 플랫폼의 enum과 어떻게 대응하는지를 코드에 적어요. 공급자를 추가했는데 대응이 빠지면 빌드나 테스트에서 오류가 나게 해요. evidence의 값으로 본문에서 든 예는 sdk_auto, sdk_callback, app_visible이에요. 위 예시의 sdk_impression과 이 세 값이 어떻게 대응하는지는 이 글에서 정의하지 않았어요.
selection_source는 이벤트가 가리키는 대상에 맞춰 계산해요. 강제로 고른 공급자가 실패한 뒤 다른 공급자의 광고가 표시됐다면, 실패의 출처와 impression의 출처가 다를 수 있어요. 요청이 이어진다고 같은 source를 복사해 붙이면 뜻이 틀어져요.
광고 재고에 기대지 않고 확인하기
실제 광고가 내려오기를 기다리면 확인이 재고 상황에 좌우돼요. callback을 테스트에서 직접 주입하면 그 의존을 없앨 수 있어요. 아래는 그렇게 확인할 때의 기댓값이고, 실제로 실행해서 통과한 기록은 아니에요.
- load만 성공하면 impression은 0건이에요.
- SDK가 한 번 통지하면 1건이에요.
- 같은 인스턴스가 다시 통지해도 1건이에요.
- 새 광고로 refresh된 뒤에는 누적 2건이에요.
- 자동 수집을 하는 공급자에서는 수동 기록이 0건이에요.
- 요청하기 전에 건너뛴 공급자의 실패는 0건이에요.
- 실패한 뒤 다른 공급자가 표시되면 실패 1건과 노출 1건이고, source는 각각 계산해요.
- unmount 뒤에 늦게 온 통지는 0건이에요.
- 보이지 않는 상태에서 온 observer의 첫 통지는 0건이에요.
- 독립된 슬롯 두 개가 각각 표시되면 2건이에요.
이 규약에서 SDK callback을 가장 앞선 근거로 삼은 것, 식별 단위, 내부 enum과 source의 설계는 문서를 읽고 내린 제 해석이에요. 네트워크의 과금이나 정산을 보장하지 않아요. Web의 자체 가시성 규칙은 threshold, 머문 시간, 영역을 벗어났을 때의 초기화까지 검증해야 하는데, 한순간의 entry만으로는 얼마나 오래 보였는지 판정할 수 없어요. trackVisibility는 제한적이고 실험적인 상태여서 반드시 필요한 의존 대상에서는 뺐어요. 수익 통지를 앞선 impression에 어떻게 연결할지, 통지가 도착하는 순서를 어떻게 처리할지는 이 글에서 다루지 않아요.