목차
서비스를 열고 나면 "사람들이 어디까지 오고 어디서 나가나요?"라는 질문이 가장 먼저 와요. 이 글은 작은 팀이 외부 분석 서비스 없이, 이벤트 테이블 하나와 BI 도구로 그 질문에 답하는 방법을 순서대로 정리해요.
혹시 이런 고민 있으신가요?
- 버튼마다 클릭을 다 기록하고 싶은데, 무엇을 남기고 무엇을 버려야 할지 모르겠어요.
- 웹 분석 도구의 숫자와 우리 데이터베이스의 숫자가 달라서 어느 쪽을 믿어야 할지 헷갈려요.
- 퍼널을 만들어 봤는데, 정작 사용자가 가장 많이 쓰는 흐름은 하나도 잡히지 않았어요.
이 글을 읽으면
- "이 숫자가 틀리면 가설이 흔들리는가"라는 질문 하나로 제품 이벤트와 페이지뷰 분석을 나눌 수 있어요.
- 이벤트를 한 테이블에 JSON 속성과 함께 쌓고, 이름과 속성을 나중에 후회하지 않게 정할 수 있어요.
- Metabase 에서 일별 단계 수, 전환 퍼널, 입구별 코호트 질문을 SQL 로 만들고 직접 검증할 수 있어요.
1단계. 가설에서 퍼널 단계를 뽑기
퍼널은 사용자가 목표까지 거쳐 가는 단계를 순서대로 늘어놓고, 단계마다 몇 명이 남는지 세는 방법이에요. 깔때기처럼 아래로 갈수록 좁아져서 퍼널이라고 불러요.
단계는 화면 목록에서 뽑지 말고, 서비스가 증명하려는 가설에서 뽑아요. 가설은 "이 기능이 있으면 사람들이 이렇게 행동할 것이다"라는 한 문장이에요. 그 문장이 맞는지 판단하는 데 필요한 지점만 단계가 돼요.
예를 들어 "사진 한 장으로 도장 이미지를 만들어 문서에 붙일 수 있으면 무료로도 퍼진다"는 가설이라면 단계는 이렇게 잡을 수 있어요.
| 단계 | 이벤트 이름 | 이 단계가 답하는 것 |
|---|---|---|
| 흐름 시작 | flow_started | 첫 화면의 제안이 사람을 움직였는가 |
| 이미지 완성 | image_completed | 핵심 기능이 제대로 동작했는가 |
| 문서 열기 | document_opened | 다음 단계로 이어 갈 이유가 있었는가 |
| 내려받기 | document_downloaded | 결과물을 가져갔는가 |
이름은 <대상>_<끝난 일> 처럼 과거형으로 통일하면 좋아요.
단계를 정할 때 두 가지를 같이 봐요. 첫째, 중간에서 끝나도 성공인 길이 있는지예요. 한 서비스에서는 이미지 한 장만 내려받고 나가는 사람을 이탈로 세고 있었는데, 가설로 보면 그 사람은 원하는 것을 얻어 간 성공이었어요. 그래서 그 지점에 이벤트를 따로 두었어요.
막대 길이는 상대적인 양이고, 중간에서 끝나는 성공은 따로 세요
둘째, 첫 화면이 실제로 앞세우는 흐름을 세고 있는지예요. 저희는 첫 화면을 바꿔 다른 흐름을 앞에 내세웠는데 퍼널은 예전 흐름만 세고 있었어요. 그러자 방문은 있는데 퍼널 첫 단계가 0 으로 읽히고, 그 숫자를 근거로 엉뚱한 버튼을 키우자는 제안이 되풀이됐어요. 첫 화면을 바꾸면 퍼널 정의도 같이 봐야 해요.
TIP
단계마다 "이 단계가 답하는 것" 칸을 채워 보세요. 한 줄로 못 쓰는 단계는 가설과 상관없는 단계일 가능성이 커요.
2단계. 제품 이벤트와 페이지뷰 분석을 가르기
기록할 곳은 보통 두 군데예요. 하나는 Umami 같은 웹 분석 도구로, 어떤 페이지를 몇 명이 봤는지 세요. 다른 하나는 여러분이 직접 운영하는 이벤트 테이블이에요. 이 테이블은 사용자 아이디와 이어 보고, 결제나 저장 같은 업무 데이터와 조인하려고 두는 곳이에요.
가르는 기준은 버튼 종류가 아니라 질문이에요. "이 숫자가 틀리면 가설이 흔들리는가?" 흔들린다면 제품 이벤트로 테이블에 남겨요. 흔들리지 않는다면 메뉴 이동, 탭, 토글, 슬라이더, 대화상자 닫기 같은 클릭이고, 웹 분석 도구의 커스텀 이벤트로 세요. Umami 는 버튼에 data-umami-event 속성을 붙이는 것만으로 클릭을 세 줘요(Umami 이벤트 추적 문서).
한 사건은 한 곳에만 기록해요
<!-- 가설과 상관없는 클릭: 웹 분석 도구가 세요 --><button data-umami-event="settings-tab">설정</button> <!-- 가설이 걸린 클릭: 코드에서 제품 이벤트 하나만 보내요 --><a href="/start" onclick="track('flow_started', { point: 'landing' })">무료로 시작하기</a>한 사건을 두 곳에 보내지 않는 게 핵심이에요. 같은 일을 재는 숫자가 두 개 생기면 어느 쪽이 맞는지 아무도 모르게 돼요. 그래서 저희는 제품 이벤트가 이미 나가는 저장, 취소 버튼에는 웹 분석 이름을 일부러 붙이지 않았어요.
페이지 도착도 마찬가지예요. 흐름 시작 이벤트는 버튼을 누른 순간에 보내고, 시작 페이지에 도착한 순간에는 보내지 않아요. 도착은 이미 페이지뷰로 세고 있으니까요. 검색이나 즐겨찾기로 바로 들어온 사람은 시작 이벤트 없이 페이지뷰만 남는데, 그게 맞아요. 그 사람은 버튼을 누르지 않았어요.
클릭을 전부 이벤트 테이블로 보내면 비용도 생겨요. 로그인하지 않은 요청은 주소별로 횟수를 제한하는 경우가 많은데, 이 한도는 보통 한 사람이 퍼널을 한 번 도는 데 맞춰 잡아요. 사무실이나 공용 와이파이처럼 여러 명이 한 주소로 들어오면, 클릭까지 다 보낼 때 퍼널을 다 돌기도 전에 한도에 걸려요.
TIP
한도를 정할 때는 한 흐름이 낼 수 있는 이벤트의 최댓값을 직접 세 보세요. 한 번만 나는 이벤트는 1, 재시도할 수 있는 이벤트는 몇 번 더 시도하는 정도로 넉넉히 잡아 더하면 돼요. 그 합을 테스트 데이터로 남겨 두면 이벤트가 늘 때 한도가 아직 넉넉한지 테스트가 알려 줘요.
3단계. 이벤트를 한 테이블에 JSON 속성과 함께 쌓기
이벤트 테이블은 하나면 충분해요. 이벤트 이름을 열의 값으로 받고, 이벤트마다 다른 정보는 JSON 속성 칸 하나에 담아요. 이렇게 하면 새 이벤트를 더할 때 테이블 구조를 바꾸는 마이그레이션이 필요 없어요.
Postgres 라면 이 정도로 시작할 수 있어요. jsonb 는 JSON 을 파싱된 형태로 저장해 안쪽 값으로 걸러 낼 수 있는 자료형이에요.
create table events ( id bigserial primary key, service text not null, -- 어느 서비스에서 났는지 name text not null, -- 이벤트 이름 user_id text, -- 로그인한 사용자라면 그 아이디 anonymous_id text, -- 로그인 전 방문자 식별자 properties jsonb not null default '{}', occurred_at timestamptz not null, -- 브라우저에서 일이 일어난 시각 recorded_at timestamptz not null default now() -- 서버가 받은 시각);여러 서비스가 한 테이블을 쓴다면 모든 이벤트에 서비스 이름을 꼭 실어요. 다른 서비스 화면에 있는 배너 클릭이라도, 그 배너가 놓인 서비스의 이름으로 기록해야 그 서비스의 퍼널 조회에 잡혀요.
받는 쪽 API 에는 허용 목록을 둬요. 이벤트 이름마다 받을 속성의 모양을 정해 두고, 목록에 없는 이름이나 정해지지 않은 속성이 오면 거절해요. 저희는 목록에 없는 값을 받으면 400 으로 돌려보내요.
실패는 이유마다 이름을 따로 만들지 말고 한 이름에 이유 속성을 두는 게 좋아요. "몇 번 실패했나"가 가장 자주 묻는 질문인데, 이름이 여섯 개면 여섯 개를 더해야 해요. 이유별로 나눠 보는 일은 properties ->> 'reason' 으로 묶으면 쿼리 한 줄이에요.
속성에 무엇을 넣지 않을지도 정해요. 파일 이름, 사용자가 입력한 글, 오류 메시지에 딸려 온 문자열은 사용자의 내용이에요. 저희는 문서를 다루는 서비스에서 쪽수와 파일 크기 같은 숫자만 남기고 파일 이름은 넣지 않아요.
참고
이벤트 이름은 붙이기 전에 정하는 게 가장 싸요. 이미 쌓인 행은 옛 이름으로 남기 때문에, 이름을 바꾸면 그때부터 모든 질문이 두 이름을 합쳐 세도록 고쳐야 해요. 속성에 값 하나를 더하는 건 싸지만, 있던 값의 뜻을 바꾸는 건 옛 행의 뜻을 조용히 바꾸는 일이라 새 값을 만드는 편이 나아요.
4단계. Metabase 에서 일별 단계 수와 전환 퍼널 만들기
Metabase 는 데이터베이스에 붙어 SQL 이나 클릭만으로 질문을 만들고 차트와 대시보드로 보여 주는 오픈소스 BI 도구예요. BI 는 Business Intelligence 의 줄임말로, 쌓인 데이터를 사업 판단에 쓰는 도구를 말해요. 같은 Postgres 에 붙이면 이벤트와 업무 테이블을 한 쿼리로 조인할 수 있어요. 연결에는 읽기만 되는 계정을 따로 만들어 쓰는 게 좋아요.
첫 질문은 날짜별 단계 수예요. 새 SQL 질문을 열고 이렇게 적어요.
select date_trunc('day', occurred_at at time zone 'Asia/Seoul') as day, name as stage, count(distinct coalesce(anonymous_id, user_id)) as visitors, count(*) as event_countfrom eventswhere service = 'example' and name in ('flow_started', 'image_completed', 'document_opened', 'document_downloaded') and {{occurred_at}}group by 1, 2order by 1, 2at time zone 은 날짜를 여러분 사용자의 시간대로 자르기 위해서예요. 빼면 자정 근처의 이벤트가 전날로 넘어가요. coalesce(anonymous_id, user_id) 는 로그인 전에는 방문자 식별자로, 없으면 사용자 아이디로 사람을 세는 방법이에요.
{{occurred_at}} 는 Metabase 의 변수예요. 오른쪽 변수 칸에서 유형을 Field Filter 로 두고 occurred_at 열에 연결하면, 대시보드에 날짜 선택 위젯이 생기고 비워 두면 전체 기간을 읽어요(Metabase SQL 변수 문서). 결과는 막대 차트로 바꿔 X 축에 day, 시리즈에 stage, Y 축에 visitors 를 둬요.
두 번째 질문은 전환 퍼널이에요. 앞 단계 이벤트가 있는 사람만 다음 단계에 세요.
with ev as ( select name, coalesce(anonymous_id, user_id) as actor from events where service = 'example' and name in ('flow_started', 'image_completed', 'document_downloaded') and {{occurred_at}}),started as ( select distinct actor from ev where name = 'flow_started'),completed as ( select distinct actor from ev where name = 'image_completed' and actor in (select actor from started)),downloaded as ( select distinct actor from ev where name = 'document_downloaded' and actor in (select actor from completed))select '1. 시작' as stage, count(*) as visitors from startedunion allselect '2. 완성', count(*) from completedunion allselect '3. 내려받기', count(*) from downloadedorder by 1시각화에서 Funnel 을 고르면 단계 사이 비율까지 같이 보여 줘요. Metabase 의 퍼널 차트는 단계 열 하나와 수치 열 하나가 있으면 그려지고, 단계가 순서대로 줄어든다는 전제에서만 뜻이 있어요(Metabase 퍼널 차트 문서).
이 방식은 앞 단계를 건너뛴 사람을 빼요. 예를 들어 첫 화면에서 바로 둘째 단계로 간 사람은 전환 퍼널에 안 잡혀요. 그래서 단계별 전체 수는 첫 번째 일별 질문에서 보고, 두 질문을 한 대시보드에 나란히 둬요.
TIP
대시보드를 만든 뒤에는 여러분이 직접 흐름을 처음부터 끝까지 한 번 돌려 보세요. 새로고침했을 때 오늘 막대의 모든 단계와 전환 퍼널의 모든 단계가 1 씩 늘면 계측이 제대로 된 거예요. 늘지 않는 단계가 있으면 그 자리의 이벤트가 빠진 거예요.
5단계. 속성으로 코호트를 나눠 이탈 자리를 찾기
코호트는 같은 조건을 가진 사람들의 묶음이에요. "첫 화면 버튼으로 들어온 사람들"과 "소개 페이지 버튼으로 들어온 사람들"이 각각 코호트예요. 전체 전환율이 낮다는 것만으로는 무엇을 고칠지 모르지만, 묶음별로 나누면 어느 입구가 약한지 보여요.
이걸 하려면 이벤트를 보낼 때 묶음을 가를 값을 속성에 실어 둬야 해요. 저희는 흐름 시작 이벤트에 "어느 화면의 어느 자리에서 눌렀는지"를 point 속성으로 실어요. 그러면 추가 계측 없이 입구별 전환을 쿼리 하나로 볼 수 있어요.
with starts as ( select distinct coalesce(anonymous_id, user_id) as actor, properties ->> 'point' as entry_point from events where service = 'example' and name = 'flow_started' and occurred_at >= now() - interval '14 days'),done as ( select distinct coalesce(anonymous_id, user_id) as actor from events where service = 'example' and name = 'document_downloaded' and occurred_at >= now() - interval '14 days')select s.entry_point, count(*) as started, count(d.actor) as downloaded, round(count(d.actor)::numeric / nullif(count(*), 0) * 100, 2) as conversion_pctfrom starts sleft join done d on d.actor = s.actorgroup by s.entry_pointorder by started descleft join 은 내려받지 않은 사람도 시작한 사람 수에 남기려고 쓰고, count(d.actor) 는 짝이 있는 사람만 세요. nullif(..., 0) 은 0 으로 나누는 오류를 막아 줘요. 한 사람이 여러 입구로 들어왔다면 입구마다 한 번씩 세어진다는 점은 알고 읽어야 해요.
같은 방법은 실패와 취소에도 통해요. 저희는 저장 취소 이벤트에 "로그인 창이 뜨기 전에 그만뒀는지, 대화상자에서 이름까지 적다가 그만뒀는지"를 속성으로 실어요. 앞쪽이 많으면 로그인 요구가 벽이고, 뒤쪽이 많으면 저장 자체를 망설이는 거예요. 같은 "취소" 숫자라도 고칠 곳이 완전히 달라져요.
TIP
속성은 나중에 무엇으로 나눠 볼지 먼저 물어보고 정하세요. "이 값으로 묶었을 때 다른 결정을 내리게 되는가"에 답할 수 있는 속성만 남기면, 허용 목록도 짧게 유지되고 쿼리도 단순해져요.
자주 묻는 질문
Q1. 처음부터 모든 클릭을 이벤트 테이블에 넣어 두면 나중에 편하지 않나요?
편해 보이지만 대가가 커요. 한도가 금방 차서 정작 퍼널 이벤트가 거절될 수 있고, 익명 사용자가 여러분 데이터베이스에 넣을 수 있는 행 수도 늘어나요. 가설과 상관없는 클릭은 웹 분석 도구에 맡기고, 가설이 걸린 사건만 테이블에 남기세요.
Q2. 웹 분석 도구의 퍼널과 Metabase 퍼널의 숫자가 달라요. 뭐가 틀린 건가요?
둘 다 맞을 수 있어요. 웹 분석 도구는 페이지 방문을 세고, 이벤트 테이블은 로그인 여부나 실제 완료 같은 제품 사건을 세요. 저희는 웹 분석 쪽 퍼널 보고서의 설명 칸에 "여기는 페이지 방문만 센다, 두 수가 다른 것이 정상이다"라고 적어 두었어요.
Q3. 이벤트가 쌓이면 별도 분석용 데이터베이스가 필요하지 않을까요?
저희는 운영 중인 Postgres 한 대에 분석용 스키마를 따로 두고, 그 스키마에만 쓸 수 있는 계정으로 이벤트를 받아요. 새 저장소를 들이기 전에 지금 있는 것으로 안 되는지부터 재 보는 편이 작은 팀에게 가벼워요.
정리
- 퍼널 단계는 화면 목록이 아니라 가설에서 뽑고, 첫 화면이 앞세우는 흐름을 세는지 확인해요.
- "이 숫자가 틀리면 가설이 흔들리는가"로 제품 이벤트와 웹 분석을 나누고, 한 사건은 한 곳에만 보내요.
- 이벤트는 한 테이블에 이름과 JSON 속성으로 쌓고, 허용 목록으로 이름과 속성을 지켜요.
- Metabase 에는 일별 단계 수와 전환 퍼널을 나란히 두고, 직접 한 번 돌려 숫자가 느는지 확인해요.
- 입구나 실패 이유 같은 속성으로 코호트를 나눠, 고칠 자리를 좁혀요.
거창한 분석 도구보다, 가설 하나와 잘 정한 이벤트 몇 개가 작은 팀에게 더 빨리 답을 줘요.