목차
GA4 태그는 깔려 있었는데 보는 사람이 없었습니다. 무엇을 봐야 할지 정한 사람도 없었고, 화면에서 볼 수 있는 지표는 페이지 접속 수 정도였어요. 제품팀이 기능 반응을 듣는 길은 정기 VOC뿐이었습니다.
페이지뷰로는 안 보이던 것
우리 제품은 페이지 하나 안에서 검색, 필터, 상세 보기, 관심 목록 담기, 다운로드가 다 일어납니다. 페이지뷰로는 이 안에서 무슨 기능이 쓰였는지 남지 않아요. 어떤 기능이 실제로 쓰이는지 물으면 근거가 인터뷰밖에 없었습니다.
그래서 목표를 하나로 잡았습니다. 화면 부품마다 얼마나 쓰이는지 집계하는 것이에요. 알고 싶은 건 기능별 사용량이었고, 누가 무엇을 눌렀는지는 처음부터 관심 밖이었습니다. 이 글의 수치도 전부 내부 트래픽을 뺀 GA4 집계입니다.
트리거 하나, 변수 하나
개발팀에 이벤트 수백 개를 심어 달라고 할 여력은 없었어요. 그래서 이미 화면에 그려진 HTML 구조를 거꾸로 읽기로 했습니다.
GTM에는 모든 클릭 트리거 하나와 맞춤 JavaScript 변수 하나만 둡니다. 변수는 클릭된 요소에서 closest()로 부모를 거슬러 올라가요. 어느 페이지의 어느 영역, 어느 부품의 어느 버튼인지 판정해서 라벨 문자열을 돌려줍니다. 판정에 실패하면 아무것도 보내지 않아요.
원리를 보여 주려고 다시 쓴 예시입니다. 선택자와 라벨 이름은 실제와 달라요.
// GTM 맞춤 JavaScript 변수 (예시)
function () {
var el = {{Click Element}};
if (!el || !el.closest) return undefined;
// 1. 어느 영역인가
var area = null;
if (el.closest(".search-panel")) area = "srch";
else if (el.closest(".detail-modal")) area = "dtl";
if (!area) return undefined; // 판정 실패면 보내지 않는다
// 2. 어느 버튼인가
var btn = el.closest("button, a");
var text = btn ? btn.textContent.trim() : "";
if (text === "저장") return area + "Save";
if (text === "다운로드") return area + "Download";
return area + "All"; // 모르는 클릭은 영역 폴백으로
}
라벨은 주소처럼 짓습니다. 영역, 모드, 위치, 요소 순서로 붙여서 srchResultSave라면 검색 화면 결과 목록의 저장 버튼이에요. 라벨만 읽어도 화면 위치가 나옵니다.
영역마다 마지막에는 …All 폴백을 둡니다. 규칙에 없는 클릭이 폴백에 쌓이기 시작하면 화면이 바뀌었다는 신호로 읽어요. 같은 클릭을 유료 회원과 무료 회원으로 나눠 볼 수 있게 사용자 속성도 붙였습니다.
품이 가장 많이 든 건 매핑이었습니다. 6월 9일에 버전 관리를 시작할 때 이미 21개 영역을 덮고 있었어요. 헤더, 왼쪽 메뉴, 데이터 화면 일곱 개, 상세 모달, 내 정보, 결제 모달 같은 곳입니다. 11월 12일 기준 라벨은 679개, 명세표는 669행이에요.
정확해야 하는 전환은 다른 길로 받습니다. 7월 31일부터 가입과 결제는 개발팀이 심은 dataLayer 이벤트로 받아요. 화면 요소로 결제를 잡으면 허수가 잡히고 매출이 빠졌습니다. 주소로 가입을 잡으면 새로고침할 때 두 번 셌고요. 이 이관 이야기는 앞 글에 따로 적었습니다. 10월 22일에는 랜딩과 블로그용 비회원 분류기를 따로 떼어 버전 관리에 넣었어요.
네 곳이 같아야 게시한다
작업 순서는 고정해 두었습니다.
- 운영 화면의 DOM을 보고 명세표(TSV)를 쓴다
- 명세를 코드에 반영하고, 직전본을 스냅샷으로 남긴다
- CHANGELOG를 쓰고 GTM 미리보기로 검증한 뒤 게시한다
- 운영에 배포된 컨테이너에서 분류기 코드를 받아 로컬 코드와 해시를 대조한다
| 항목 | 값 |
|---|---|
| 기록 기간 | 06-09(v1) ~ 11-12(v34) |
| GTM 컨테이너 버전 | 169(06-30) → 200(11-12) |
| 롤백 스냅샷 | 회원용 16개, 비회원용 5개 |
| 날짜별 명세표 | 19개 |
해시 대조는 사고를 겪고 나서 생긴 규칙입니다. 8월 14일에 요금제 화면 버튼 하나의 선택자를 고쳤는데, 수정이 로컬 코드에만 있고 GTM 변수에는 들어가지 않았어요. 8월 26일까지 12일 동안 그 버튼 클릭이 집계되지 않았습니다. 그 뒤로는 게시할 때마다 로컬, GTM 작업공간, 운영본 세 곳의 해시를 맞춥니다.
개발 배포에는 두 가지로 대응합니다. 배포 전에 옛 선택자와 새 선택자를 함께 넣어 미리 게시해 두는 방법이 하나예요. 다른 하나는 배포 직후 운영 DOM을 확인하고 옛 선택자를 걷어 내는 방법입니다. 라벨 구조가 바뀌어 건수가 끊긴 시점은 전부 대시보드의 단절 표에 적어 둡니다.
결제 직전 잠금 표시를 누르는 클릭은 라벨 끝을 같게 지었어요. 그러면 GA4에서 정규식 하나로 20종을 한꺼번에 묶을 수 있습니다. 팀이 같이 쓸 수 있게 명세 공유 시트와 클릭 사전도 관리해요. 루커 스튜디오의 클릭 페이지가 이 사전을 데이터 소스로 읽습니다.
집계가 닿은 곳
8월 말에는 유료 회원의 기능 사용을 처음으로 숫자로 정리해 제품팀에 올렸습니다. 90일 클릭 기준으로 상위 10%가 클릭의 약 60%를 차지했어요. 많이 쓰는 쪽도 한 부류가 아니었고, 주로 쓰는 화면에 따라 세 갈래로 나뉘었습니다. VOC로는 나올 수 없는 사용 분포였어요.
이 분석에서 나온 제안 하나는 소기능 기획으로 좁혀 9월 1일 제품팀에 발표했습니다. 그 방향은 새 소식 알림 기능으로 출시됐어요. 계측은 출시 전에 게시해 두었고, 판정도 같은 데이터로 했습니다.
결제 화면도 다시 봤습니다. 결제 모달 전환율을 클릭 수 대신 중복을 걷어 낸 인원 기준으로 다시 셌고, 그 숫자로 개선 제안서를 썼어요. 제안은 개발 요청으로 넘어갔고, 다운그레이드와 해지 예약 철회 두 가지가 10월 1일 운영에 반영됐습니다.
유료와 무료를 가르는 화면도 찾았습니다. 상세 화면의 목록 하나를 쓰는 비율이 유료와 무료 사이에 약 10배 차이가 났어요. 영업과 마케팅이 유료 전환 메시지를 정할 때 이 차이를 근거로 씁니다.
아직 남은 숙제
가장 큰 위험은 선택자가 어긋나는 일입니다. 개발팀이 클래스 이름을 바꾸면 에러 없이 집계만 끊겨요. 10월 29일 랜딩 개편 때는 오전 9시 47분부터 11시 11분 게시 전까지 새 랜딩의 비회원 클릭이 거의 잡히지 않았습니다. 같은 날 새로 생긴 메뉴 진입점은 규칙이 없어서 왼쪽 메뉴 폴백이 하루 7건 안팎에서 499건으로 부풀었어요. 폴백 덕분에 알아챘지만, 대응은 늘 배포 뒤를 쫓아가는 식입니다.
그래서 측정을 선언형으로 바꾸려고 해요. 새 화면은 개발팀이 요소에 data-gtm-click 속성을 붙이고, GTM은 그 값을 라벨로 바꾸기만 하는 방식으로 설계했습니다. 11월 9일에는 기획자가 화면을 기획할 때 계측까지 같이 기획하는 규칙도 생겼어요. 이제 기존 화면을 어디부터 이 방식으로 옮길지 정해야 합니다.
분석 단위의 한계도 남아 있습니다. 클릭 단위로 검정하면 의사복제 때문에 유의성이 부풀려져서 배수와 방향만 믿어요. 이 문제는 판정 방법 글에 따로 정리했습니다. BigQuery가 없어서 흐름 분석은 GA4 화면 안에서만 할 수 있고, 구독 갱신은 결제 이벤트로 잡히지 않아요.
라벨 규모는 (other) 손실을 기준으로 관리합니다. 하루 고유 라벨은 평일 446~505개이고 아직 손실은 없어요. 500개는 참고선으로만 봅니다.