GA 입문 3편. GA4 의 4 측정 방법 깊이 — gtag.js (dataLayer 메커니즘 · command 4종 · 직접 통합), Google Tag Manager (Container · Tag · Trigger · Variable · Workspace · Version · Environment · Preview · sGTM), Firebase SDK (iOS · Android · Flutter · 모바일 자동 event), Measurement Protocol (HTTP endpoint · client_id 결합 · validation server · debug · server-side hit). 결정 매트릭스 + 함정까지 풀어쓴 학습 노트.
이 글은 Google Analytics 입문에서 운영까지 시리즈 3편이에요. 1편 큰 그림 의 4 측정 방법 을 짧게 봤다면, 2편 데이터 모델 다음 — 각 방법의 실전 깊이.
이번 글의 범위
GA 에 데이터 보내는 4 가지 길 의 깊이 풀이. 우리 stack 의 어디서 측정해야 하는지의 결정.
| 방법 | 어디서 | 누구 운영 |
|---|---|---|
| gtag.js | 웹 (코드 직접) | 개발자 |
| GTM | 웹 (대시보드) | 마케터 + 개발자 |
| Firebase SDK | 모바일 앱 | 개발자 |
| Measurement Protocol | 서버 | 백엔드 개발자 |
gtag.js — 직접 통합
dataLayer 메커니즘
gtag 는 내부적으로 dataLayer (페이지에 둔 글로벌 배열) 를 씁니다. 모든 GA hit 가 dataLayer 에 push 돼요.
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){ dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', 'G-XXXXXXXX');
</script>
gtag 함수의 본질 — dataLayer.push 의 wrapper. 별도 magic 없음.
gtag command 4종
// 1. config — Property 초기화 (앱 시작 시 1회)
gtag('config', 'G-XXXXXXXX', {
send_page_view: false, // 자동 page_view 끄기
user_id: 'user-12345'
});
// 2. event — 이벤트 박기
gtag('event', 'product_viewed', {
product_id: 'P-123',
category: 'shoes'
});
// 3. set — Default parameter (이후 모든 event 에 적용)
gtag('set', 'user_properties', {
user_tier: 'premium'
});
// 4. consent — Consent Mode (2편)
gtag('consent', 'update', {
analytics_storage: 'granted'
});
config 의 옵션
gtag('config', 'G-XXXXXXXX', {
send_page_view: true, // 자동 page_view (default true)
cookie_domain: 'auto', // cookie 도메인
cookie_flags: 'SameSite=None; Secure', // SameSite (cross-site 시)
cookie_expires: 63072000, // cookie 만료 (초, default 2년)
anonymize_ip: true, // IP 익명화 (GA4 자동)
allow_google_signals: true, // Google Signal 활성
allow_ad_personalization_signals: true
});
SPA (Single Page App) 의 함정
여기서 시험 함정이 하나 있어요. SPA (React · Vue · Next.js) 는 URL 변경 이 full page reload 없이 일어나요. 그래서 자동 page_view event 가 최초 1회만 박힙니다.
해결 — 수동 page_view:
// React Router · Next.js 의 route change hook
function trackPageView(url) {
gtag('event', 'page_view', {
page_location: url,
page_title: document.title
});
}
// React Router 예
useEffect(() => {
trackPageView(window.location.href);
}, [location]);
또는 gtag config 의 send_page_view: false 로 자동을 끄고 모든 page_view 를 수동 으로 박는 방식.
Google Tag Manager (GTM) — 대시보드 운영
GTM 의 구조
[GTM Account]
↓
[Container] — 하나의 사이트/앱
├─ [Tag] — 발사할 코드 (GA4 · Facebook Pixel · Hotjar 등)
├─ [Trigger] — Tag 발사 조건
├─ [Variable] — Tag·Trigger 안에서 쓰는 값
└─ [DataLayer] — 사이트에서 GTM 으로 전달하는 데이터
Container = 태그 관리 설정의 저장소. — 공식 docs
Tag — 발사할 코드
Tag 종류 예:
- Google Analytics: GA4 Event
- Google Ads: Conversion Tracking
- Facebook Pixel
- Hotjar Tracking
- Custom HTML
- Custom JavaScript
수많은 vendor 의 표준 Tag 템플릿. 코드 한 줄 안 박고 마케터가 직접 설정.
Trigger — 발사 조건
대표 Trigger:
- Page View (모든 페이지뷰)
- Page View — DOM Ready
- Page View — Window Loaded
- Click — All Elements
- Click — Just Links
- Form Submission
- Scroll Depth (25% · 50% · 75% · 90%)
- Element Visibility
- YouTube Video
- Custom Event (dataLayer push)
- JavaScript Error
- Timer (N 초 후)
각 Trigger 가 조건 을 정의. 매칭 시 해당 Tag 발사.
Variable — 동적 값
Built-in Variables (자동 제공):
- Page URL · Page Hostname · Page Path
- Click URL · Click Text · Click ID · Click Classes
- Form ID · Form URL
- Random Number · Event
User-defined Variables (직접 정의):
- DataLayer Variable (dataLayer 에서 값 추출)
- JavaScript Variable (window.xxx)
- Constant
- Lookup Table (a → b 매핑)
- Regex Table
- Custom JavaScript (함수 정의)
대표 사용 — Tag · Trigger 안에서 {{Page Path}} · {{Click Text}} 같이 동적 참조.
DataLayer — GTM 의 데이터 입력
// 사이트 코드에서 GTM 로 데이터 전달
window.dataLayer = window.dataLayer || [];
dataLayer.push({
event: 'purchase', // GTM 의 Trigger 가 이걸 듣는다
transaction_id: 'T-12345',
value: 50000,
currency: 'KRW'
});
GTM 의 Custom Event Trigger 가 이 dataLayer push 를 감지 → GA4 Tag 발사.
Workspace · Version · Environment
Workspace:
- 협업 단위 (브랜치 같은 개념)
- 여러 사람이 동시 변경 작업
Version:
- Workspace 의 snapshot
- "v15: 2026-05-17 caretive_change"
Environment:
- Development · Staging · Production
- 각 환경 다른 Version 배포
Git 같은 workflow — workspace = branch, version = commit, environment = deployment.
Preview · Publishing
Preview Mode:
- 변경 사항 = 즉시 production 적용 X
- "Tag Assistant Preview" 로 검증
- 우리 사이트에 디버그 모드로 접속
Publishing:
- Workspace → Version 생성
- Version → Environment 배포
- 변경 사항 production 활성
운영 안전선 — 프로덕션 직행 X, Preview 검증 후 Publish.
Server-side GTM (sGTM)
일반 GTM = 사용자 브라우저에서 vendor 서버로 직접 hit.
sGTM = 우리 서버 (Cloud Run · App Engine 등) 가 프록시 — 브라우저는 우리 서버로만 hit, 우리 서버가 각 vendor 로 분배.
장점은 네 갈래로 정리돼요. Cookie 통제 가 가능해 cookieless 환경에 대응할 수 있고, 우리 서버가 vendor 역할을 하니 ad blocker 회피 도 됩니다. 거기에 Server-side 데이터 보강 — PII (개인 식별 정보) 제거나 데이터 정제 — 가 한 박자 끼고, vendor 가 raw user data 를 못 보는 Privacy 통제 까지 따라옵니다.
대규모 운영 + privacy 가 중요한 환경 = sGTM 권장. 추가 비용 + 운영 부담 은 감수.
Firebase SDK — 모바일 앱
iOS (Swift)
import FirebaseCore
import FirebaseAnalytics
// AppDelegate
FirebaseApp.configure()
// Event 박기
Analytics.logEvent("product_viewed", parameters: [
"product_id": "P-123",
"category": "shoes",
"price": 50000
])
// User Property
Analytics.setUserProperty("premium", forName: "user_tier")
// User ID
Analytics.setUserID("user-12345")
// Screen 추적
Analytics.logEvent(AnalyticsEventScreenView, parameters: [
AnalyticsParameterScreenName: "ProductDetail",
AnalyticsParameterScreenClass: "ProductDetailViewController"
])
Android (Kotlin)
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
val analytics = Firebase.analytics
// Event
analytics.logEvent("product_viewed") {
param("product_id", "P-123")
param("category", "shoes")
param("price", 50000.0)
}
// User Property
analytics.setUserProperty("user_tier", "premium")
// User ID
analytics.setUserId("user-12345")
// Screen
analytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW) {
param(FirebaseAnalytics.Param.SCREEN_NAME, "ProductDetail")
}
Flutter / React Native
// Flutter
import 'package:firebase_analytics/firebase_analytics.dart';
FirebaseAnalytics analytics = FirebaseAnalytics.instance;
await analytics.logEvent(
name: 'product_viewed',
parameters: {
'product_id': 'P-123',
'category': 'shoes',
},
);
자동 수집 Event (모바일)
Firebase SDK 가 자동 박는 event:
- first_open (앱 첫 실행)
- session_start (세션 시작)
- screen_view (화면 전환)
- app_clear_data
- app_remove (앱 삭제)
- app_update (버전 update)
- in_app_purchase (앱 내 결제 — 자동)
- os_update
- notification_* (FCM 알림 관련)
설치만 해도 모바일 핵심 event 가 자동.
Crashlytics 결합
Firebase 의 Crashlytics (앱 크래시 수집 도구) 와 Analytics 가 자연 통합. 크래시 발생 → 자동 app_exception event 박힘 → GA4 에서 crash 사용자 segment 가능.
Remote Config — A/B Testing
Firebase Remote Config (서버에서 앱 설정값 원격 제어) 는 코드 배포 없이 설정값 변경 을 가능하게 합니다. Firebase A/B Testing 결합 = GA4 의 audience 기반 실험.
val remoteConfig = Firebase.remoteConfig
val buttonColor = remoteConfig.getString("button_color") // "red" or "blue"
A/B 그룹 = GA4 audience 로 자동 매핑 + conversion 측정 자동.
Measurement Protocol — Server-side Hit
HTTP Endpoint
Measurement Protocol = HTTP requests 로 GA 서버 직접 event send. — 공식 docs
# Production endpoint
POST https://www.google-analytics.com/mp/collect
?measurement_id=G-XXXXXXXX
&api_secret=YOUR_API_SECRET
# Validation server (debug)
POST https://www.google-analytics.com/debug/mp/collect
?measurement_id=G-XXXXXXXX
&api_secret=YOUR_API_SECRET
Payload 구조
{
"client_id": "555.123",
"user_id": "user-12345",
"events": [{
"name": "purchase",
"params": {
"transaction_id": "T-12345",
"value": 50000,
"currency": "KRW",
"items": [{
"item_id": "P-1",
"item_name": "Shoes",
"price": 50000
}]
}
}]
}
client_id 의 의미
Measurement Protocol 이 client_id 또는 app_instance_id 로 기존 online 활동과 결합. — 공식 docs
핵심 — server-side hit 의 client_id 가 브라우저의 GA cookie 의 client_id 와 일치 하면 → 같은 사용자로 통합.
// 브라우저에서 client_id 추출
function getClientId(callback) {
gtag('get', 'G-XXXXXXXX', 'client_id', callback);
}
// 우리 백엔드로 보냄
getClientId((clientId) => {
fetch('/api/server-event', {
method: 'POST',
body: JSON.stringify({ clientId: clientId, eventName: 'purchase', ... })
});
});
// 백엔드 → Measurement Protocol
// 같은 clientId 로 GA send → 통합 추적
Validation Server
Validation server = production 보내기 전에 payload 검증.
curl -X POST 'https://www.google-analytics.com/debug/mp/collect?measurement_id=G-XXXXXXXX&api_secret=YOUR_SECRET' \
-H 'Content-Type: application/json' \
-d '{...}'
Response — validation 결과 (error · warning · 성공).
{
"validationMessages": [
{
"fieldPath": "events[0].params.value",
"description": "value must be numeric",
"validationCode": "VALUE_INVALID"
}
]
}
→ production endpoint 보내기 전 항상 validation. typo · type 오류를 사전 발견.
DebugView
GA4 console 의 DebugView = 실시간 event 흐름 확인:
Payload 에 debug_mode: 1 추가:
{
"client_id": "...",
"events": [...],
"user_properties": {...},
"debug_mode": true // 또는 1
}
→ GA4 console 의 DebugView 에서 실시간 event 도착 확인. 디버그 환경에서만.
사용 Case
✓ 백엔드 결제 webhook → 서버에서 purchase event
✓ 환불 발생 → server-side refund event
✓ 구독 자동 결제 → 사용자 액션 없이 conversion
✓ 키오스크 · 시계 같은 client SDK 안 되는 환경
✓ CRM (고객 관계 관리) 시스템 event (이메일 open 등 — 서버에서 수집)
✓ 오프라인 conversion → 매장 결제 → online 사용자 매칭
한계
Measurement Protocol = gtag · Tag Manager · Firebase 의 enhance (보완). replace 아님. — 공식 docs
한계를 짚으면 네 가지예요. 일부 예약 event/parameter name 은 사용 X (자동 수집 전용) 이고, 일부 UI 의 custom event rule 은 MP 에 안 적용됩니다. Geo / device 정보 는 MP payload 에 명시 안 보내면 최근 client hit 의 정보 로 채워져요 (또는 session_id 결합). Real-time report 에는 조금 늦게 반영.
→ MP 만으로 완전한 추적 X. client SDK + MP 조합이 표준.
4 방법 결정 매트릭스
시나리오별
간단한 블로그/콘텐츠 사이트:
→ gtag.js 직접 (1 줄 추가)
복잡한 marketing stack:
→ GTM (web) + 여러 vendor Tag
모바일 앱:
→ Firebase SDK (자동 event + Crashlytics + Remote Config)
대규모 e-commerce:
→ GTM (web) + Firebase (app) + MP (server-side)
Privacy 중요 환경:
→ Server-side GTM (sGTM)
레거시 시스템:
→ Measurement Protocol (서버에서 직접)
결정 기준 — 5 질문
Q1: 어디서 측정? (Web · App · Server)
Q2: 누가 운영? (개발자 · 마케터 · 둘 다)
Q3: 다른 도구 (FB Pixel · Hotjar) 통합?
Q4: Privacy / Cookieless 요구?
Q5: Server-side event 필요?
대부분 큰 회사 = GTM (web) + Firebase (app) + Measurement Protocol (server) 의 조합.
결정 매트릭스
| 상황 | 권장 |
|---|---|
| 신규 + 작은 사이트 | gtag.js |
| 마케터 자율 + 여러 도구 | GTM |
| 모바일 앱 | Firebase SDK |
| Server-side event (webhook 등) | Measurement Protocol |
| Privacy 강함 + 대규모 | Server-side GTM |
| Hybrid (web + app + server) | 3 조합 |
함정 정리
사고 1: SPA 의 page_view 누락
원인 — SPA 의 route change 가 자동 감지 X.
해결 — route hook 에서 수동 gtag('event', 'page_view', ...).
사고 2: GTM Preview 안 하고 Publish
원인 — Production 직행 → 잘못 설정한 Tag 가 모든 사용자에 영향.
해결 — Preview Mode 로 본인 brower 에서 검증 후 Publish.
사고 3: Cookie consent 와 GA 의 race condition
원인 — Cookie banner 표시 와 gtag config 의 순서가 잘못 → 동의 안 받은 상태에서 cookie 박힘.
해결 — gtag consent default = denied 를 모든 다른 코드 앞 에. 동의 후 update.
사고 4: Firebase 와 GA4 Property 분리
원인 — 모바일 앱 = Firebase project, 웹 = 별도 GA4 property → 통합 X.
해결 — GA4 console 에서 Firebase project linking + 같은 property 의 다른 stream 으로 통합.
사고 5: Measurement Protocol 의 IP/Geo 누락
원인 — Server 가 hit 보낼 때 user IP 안 명시 → server IP 로 geo 추정 → 모든 사용자가 서버 위치 로 찍힘.
해결 — payload 에 user IP override 명시 (또는 client SDK 와 같은 session 결합).
사고 6: API Secret 노출
원인 — Measurement Protocol 의 api_secret 을 Client SDK 또는 git public 에 박음.
해결 — Server only. 환경 변수 + secrets manager.
사고 7: gtag 의 dataLayer 충돌
원인 — GTM 과 gtag.js 동시 설치 → dataLayer 의 event hit 충돌 (중복 전송).
해결 — 한 가지만 선택. GTM 권장 (그 안에 GA4 Tag 추가).
사고 8: Firebase debug build 의 추적
원인 — 개발 환경 (debug) 의 event 가 production property 로 박힘.
해결 — Firebase environment 별 property 분리 + debug build 의 GA collection 비활성.
사고 9: Server-side GTM 의 latency
원인 — sGTM 의 추가 hop (브라우저 → sGTM → GA) 으로 hit 지연.
해결 — sGTM 의 server location 을 사용자 근처 (Cloud Run regions) 로.
운영 권장 패턴
Pattern 1: 표준 Web 통합 — GTM
<!-- Head -->
<script>
// 1. Consent Mode default (2편)
gtag('consent', 'default', {
ad_storage: 'denied',
analytics_storage: 'denied'
});
</script>
<!-- GTM container -->
<script>
(function(w,d,s,l,i){...})(window,document,'script','dataLayer','GTM-XXXXXX');
</script>
GTM Container 안 설정:
Tag 1: GA4 Configuration (config tag)
Measurement ID: G-XXXXXXXX
Send Page View: Yes (또는 SPA 면 No)
Tag 2: GA4 Event - Purchase
Trigger: Custom Event "purchase" (dataLayer)
Variable: DLV - transaction_id
DataLayer Variable Name: ecommerce.transaction_id
Pattern 2: SPA 의 페이지 추적
// Next.js · React Router · Vue Router
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function GoogleAnalytics() {
const location = useLocation();
useEffect(() => {
if (typeof window.gtag === 'function') {
window.gtag('event', 'page_view', {
page_location: window.location.href,
page_title: document.title
});
}
}, [location]);
return null;
}
또는 GTM 의 History Change Trigger 활용.
Pattern 3: 모바일 통합
// Android Application 클래스
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
val analytics = Firebase.analytics
// 환경별 enable/disable
analytics.setAnalyticsCollectionEnabled(BuildConfig.DEBUG.not())
// User ID (로그인 후)
currentUser?.let {
analytics.setUserId(it.id)
analytics.setUserProperty("user_tier", it.tier)
}
}
}
Pattern 4: Server-side 결제 추적
# 결제 완료 webhook
@app.post('/webhooks/payment-completed')
async def on_payment_completed(payment: Payment):
# 1. 결제 처리
await process_payment(payment)
# 2. GA4 Measurement Protocol
payload = {
"client_id": payment.client_id, # 브라우저 client_id (앞서 저장)
"user_id": payment.user_id,
"events": [{
"name": "purchase",
"params": {
"transaction_id": payment.id,
"value": payment.amount,
"currency": "KRW",
"items": [
{
"item_id": item.product_id,
"item_name": item.name,
"price": item.price,
"quantity": item.quantity
}
for item in payment.items
]
}
}]
}
async with httpx.AsyncClient() as client:
await client.post(
"https://www.google-analytics.com/mp/collect",
params={
"measurement_id": GA_MEASUREMENT_ID,
"api_secret": GA_API_SECRET # env var
},
json=payload
)
Pattern 5: Validation 워크플로
# 새 event 박을 때 — 항상 validation 먼저
async def validate_event(payload):
async with httpx.AsyncClient() as client:
resp = await client.post(
"https://www.google-analytics.com/debug/mp/collect",
params={
"measurement_id": GA_MEASUREMENT_ID,
"api_secret": GA_API_SECRET
},
json=payload
)
data = resp.json()
if data.get("validationMessages"):
raise ValueError(f"GA validation failed: {data['validationMessages']}")
return True
# 사용
async def send_event(payload):
if NODE_ENV != "production":
await validate_event(payload)
await actual_send(payload)
개발 환경에서 항상 validation, production 직행 X.
Pattern 6: client_id 추출 + Server 결합
// 브라우저 → 서버 (한 번)
async function syncClientIdToServer() {
return new Promise((resolve) => {
gtag('get', 'G-XXXXXXXX', 'client_id', (clientId) => {
// localStorage 저장 (서버 API 호출 시 사용)
sessionStorage.setItem('ga_client_id', clientId);
// 또는 우리 서버로 명시 sync
fetch('/api/user/ga-client-id', {
method: 'POST',
body: JSON.stringify({ clientId })
});
resolve(clientId);
});
});
}
// 로그인 후 한 번 호출
syncClientIdToServer();
서버가 Measurement Protocol 을 쓸 때 같은 client_id 를 보내면 = 통합 추적.
시험 직전 한 번 더 — 측정 4 방법 함정 압축 노트
gtag.js
- dataLayer 의 wrapper · 내부적으로 push
- Command 4종 — config · event · set · consent
- config 옵션 — send_page_view · cookie_* · anonymize_ip · allow_google_signals
- SPA 함정 — 자동 page_view 1회만, route hook 에서 수동 박기
GTM (Google Tag Manager)
- Container · Tag · Trigger · Variable · DataLayer
- Workspace · Version · Environment (Git 같은 workflow)
- Preview · Publishing (Production 직행 X)
- Tag 종류 풍부 (GA4 · FB Pixel · Hotjar · Custom)
- Trigger — Page View · Click · Form · Scroll · Custom Event · Timer
- Variable — Built-in (Page URL · Click Text) + User-defined (DLV · JS · Lookup · Custom)
- Server-side GTM (sGTM) — 우리 서버가 프록시 (cookieless · privacy 강화 · 추가 비용)
Firebase SDK (모바일)
- iOS · Android · Flutter · React Native
- 자동 event — first_open · session_start · screen_view · in_app_purchase · ...
- Crashlytics 결합 (app_exception)
- Remote Config + A/B Testing
- GA4 property 와 linking 필요 (별도 X)
Measurement Protocol (Server-side)
- HTTP POST
/mp/collect(production) ·/debug/mp/collect(validation) - client_id 결합 — 브라우저의 GA cookie 와 일치 시 같은 사용자
- Validation server = production 보내기 전 검증 (typo · type 오류)
- DebugView = 실시간 event 확인 (debug_mode: 1)
- 사용 case — 결제 webhook · 환불 · 키오스크 · 오프라인 conversion · CRM
- gtag · GTM · Firebase 의 enhance · replace X
- API Secret = server only
결정 매트릭스
- 작은 사이트 → gtag.js
- 복잡한 marketing → GTM
- 모바일 → Firebase
- Server event → MP
- Privacy 강함 → sGTM
- 큰 회사 = GTM + Firebase + MP 조합
사고
- SPA page_view 누락 (route hook)
- GTM Preview 안 하고 Publish
- Consent 와 GA race condition
- Firebase 와 GA4 분리 (linking 필요)
- MP 의 IP/Geo 누락
- API Secret 노출 (server only)
- gtag + GTM 동시 (중복 hit)
- Firebase debug build 의 production 박힘
- sGTM latency (server location)
패턴
- 표준 Web GTM 통합 (Consent + Container + Tag)
- SPA route hook page_view
- 모바일 Firebase + 환경 분리
- Server-side 결제 추적 (MP + client_id 결합)
- Validation 워크플로 (production 전 항상)
- client_id 추출 + Server sync (cross-device)
공식 문서: Measurement Protocol GA4 · Google Tag Manager 에서 원문을 확인할 수 있어요.
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
다음 글: