sgkstudio.
엔지니어링

GA4 이탈률을 기기별로만 자르면 광고 네트워크가 한 칸에 섞인다

GA4 이탈률을 기기별로만 비교하면 서로 다른 광고 네트워크의 정반대 신호가 한 칸에서 평균으로 뭉개집니다. 우리는 데스크톱 153초 대 모바일 9초라는 숫자를 보고 「모바일이 붕괴했다」고 읽었지만, 유입 소스로 다시 자르자 네이버 광고의 모바일 방문은 네 칸 가운데 이탈률이 가장 낮았습니다.

2026-09-262026년 8월 5일 결정 기록 — 「모바일 붕괴」 판독 철회와 반증 2건GA4 Data API 조회 스크립트(scripts/ga4_query.py)의 착지×기기 참여 지표 함수와 주석Google Ads API 광고 최종 URL 조회 결과관련 커밋 7건 대조

GA4에서 모바일 광고 방문이 왜 문제로 보였나?

GA4 이탈률을 기기별로만 비교한 표가 우리를 잘못된 수리 작업 앞에 세웠습니다. 광고로 들어온 방문자가 데스크톱에서는 평균 153초를 머무는데 모바일에서는 9초 만에 떠난다는 표였고, 차이는 17배였습니다.

작은 회사에서 검색광고 예산은 곧 현금입니다. 모바일로 들어온 광고 방문자가 9초 만에 사라진다면, 광고비를 더 쓰기 전에 모바일 화면부터 고쳐야 한다는 결론이 자연스럽게 나옵니다. 우리는 이 표를 실험 설계 문서의 2절에 적고 「모바일 붕괴」라는 이름을 붙였습니다.

문제는 그 표를 다시 만들 방법이 없었다는 점입니다. 표는 GA4 화면과 일회성 쿼리로 뽑은 결과였고, 저장소 어디에도 같은 숫자를 재현하는 코드가 없었습니다. 누구도 153초와 9초를 다시 확인할 수 없었고, 그 상태로 모바일 성능 수리 계획이 세워지고 있었습니다.

이런 일이 처음은 아니었습니다. 앞서 방문자 신뢰를 높이려던 개선 시도 세 번(v1~v3)과 유료 모바일 참여 테스트는 전부 「표본이 모자라 판정할 수 없다」로 끝났습니다. 이번에도 판정 근거가 약한 숫자 위에서 작업을 시작하면 같은 실패를 되풀이할 위험이 있었습니다.

처음 잰 숫자: 데스크톱 153초 대 모바일 9초

처음 한 일은 판독을 의심하는 것이 아니라 숫자를 재현하는 것이었습니다. GA4 Data API로 착지 페이지와 기기별 참여 지표를 뽑는 `--landing-perf` 옵션을 조회 스크립트에 넣었습니다(PR #207). 최근 30일 창으로 실제 속성에 돌리자 문서 2절 표의 6줄이 전부 그대로 나왔습니다.

GA4 유료 검색 방문의 기기별 평균 체류시간 막대그래프 — 데스크톱 153초, 모바일 9초
기기로만 자른 평균 체류시간

즉 숫자 자체는 맞았습니다. 데스크톱 153초, 모바일 9초, 17배라는 값은 GA4가 실제로 내는 값이었습니다. 이 단계에서 멈췄다면 「재현까지 확인했으니 모바일 수리에 들어간다」로 끝났을 테고, 실제로 그 계획이 다음 순서에 있었습니다.

재현 도구를 만들면서 평균과 비율 옆에 세션 단위의 원자 지표도 같이 뽑게 했습니다. 참여 세션 수(engagedSessions), 페이지뷰(screenPageViews), 총 참여 시간(userEngagementDuration)입니다. GA4는 10초 이상 머물거나, 페이지를 2개 이상 보거나, 전환이 1건 이상 있으면 그 세션을 참여로 인정합니다. 그러니 이탈률은 사실상 세션마다 참여 여부를 0과 1로 찍은 결과의 집계입니다.

  • 참여 세션 수 — 이탈률의 분자입니다. 비율 뒤에 몇 세션이 서 있는지 보입니다.
  • 페이지뷰 — 10초 타이머와 독립적인 참여 경로입니다. 모바일에서 앱 전환으로 마지막 참여 신호가 빠져도 페이지뷰는 남으므로, 체류시간 급락이 측정 착시인지 실제 이탈인지 가르는 대조 축이 됩니다.
  • 총 참여 시간 — 평균의 분자입니다. 평균이 긴 세션 한두 개에 끌려간 것인지 보입니다.

데이터에 섞이는 잡음도 먼저 걷었습니다. 이틀 전인 8월 3일에 최근 3일을 재 보니, 대표 본인이 확인차 접속한 김포 지역 데스크톱 방문이 25세션으로 전체 53세션의 47%, 페이지뷰 140건 중 95건으로 68%를 차지했습니다. 그래서 조회 스크립트는 기본값으로 이 내부 방문과 개발용 호스트의 방문을 빼고 셉니다.

세운 가설: 모바일 첫 로딩이 느리거나, 광고 문구와 첫 화면이 어긋났다

모바일 방문이 9초 만에 끝나는 이유로 우리는 몇 가지 가설을 세워 두고 있었습니다. 문서에서 번호를 붙여 관리한 가설 가운데 이 글과 관계있는 것은 두 개입니다.

「모바일 붕괴」를 설명하려고 세운 가설
가설내용검증 방법
H1 콜드 로드모바일에서 첫 로딩이 느려 화면이 뜨기 전에 떠난다모바일 성능 측정 후 수리
H2 키워드와 첫 화면 제목 불일치광고 검색어와 착지 페이지 제목이 달라 바로 뒤로 간다광고그룹별 검색어와 페이지 제목 대조

여기에 직전 작업에서 나온 판정이 하나 더 얹혀 있었습니다. 광고 게시 스크립트의 134번째 줄에 광고 최종 URL이 `https://www.sgkstudio.co.kr/ai/chatbot`으로 적혀 있었고, www 주소는 ai 서브도메인으로 308 리다이렉트를 거칩니다. 그래서 「모든 유료 클릭이 리다이렉트를 한 번 더 타느라 모바일에서 늦게 뜬다」는 판정이 H1을 받쳐 주고 있었습니다.

세 가지가 한 방향을 가리켰기 때문에 결론은 단단해 보였습니다. 모바일 화면이 느리고, 리다이렉트까지 한 번 더 타니, 모바일 성능부터 고치자는 순서였습니다.

가설이 틀린 지점: 「30일 데이터」도, 리다이렉트도 사실이 아니었다

판독이 흔들린 계기는 사소했습니다. 재현 도구가 30일 창으로 돌았으니, 그 30일 동안 광고가 실제로 몇 날 집행됐는지 확인해 본 것입니다.

구글 광고 클릭 발생 3일, 19클릭, 136,294원, 마지막 유료 방문 뒤 7일 연속 유료 유입 0을 보여 주는 수치 카드
30일 창 안에 실제로 들어 있던 광고 집행

구글 광고는 30일 내내 돈 것이 아니었습니다. 클릭이 발생한 날은 7월 26일부터 28일까지 3일뿐이었고, 그 3일에 19클릭·136,294원을 썼습니다. 광고그룹별로는 「홈페이지제작」 10클릭, 「AI챗봇」 9클릭이었습니다. 네이버 광고는 7월 17일부터 29일까지 돌았습니다. 마지막 유료 방문은 7월 29일이었고, 조사한 8월 5일 기준으로 7일 연속 유료 유입이 0이었습니다.

조회 창의 길이와 데이터가 실제로 쌓인 기간은 다른 이야기입니다. 30일 창이라고 적어 두면 읽는 사람은 한 달 동안 고르게 쌓인 데이터를 떠올리지만, 구글 쪽 숫자는 사실상 사흘치였습니다. 사흘 동안 들어온 소수의 방문이 한 달짜리 표의 한 칸을 전부 채우고 있었고, 그 칸이 판독 전체를 끌고 갔습니다.

리다이렉트 판정도 무너졌습니다. Google Ads API로 광고 계정을 직접 조회하니, 켜져 있는(ENABLED) 광고의 최종 URL은 전부 `ai.sgkstudio.co.kr`로 곧바로 들어가고 있었습니다. www를 가리키던 주소는 게시 스크립트 안의 상수일 뿐 계정에 반영돼 있지 않았고, 도메인 루트를 가리키는 광고는 전부 일시중지 상태였습니다.

광고 계정은 사람과 도구가 수시로 바꾸는 상태다. 소스 코드의 상수를 인용해서 계정의 현재 상태를 단정하면 안 된다.

이 두 반증으로 「30일치 광고 데이터에서 모바일이 무너졌다」는 문장의 주어와 기간이 둘 다 흔들렸습니다. 남은 질문은 하나였습니다. 그렇다면 모바일 9초는 대체 어디서 나온 숫자인가.

진짜 원인은 무엇이었나 — 기기 축이 광고 네트워크와 섞였다

진짜 원인은 모바일이 아니라 자르는 축이었습니다. 표의 유료 검색 60세션은 구글 광고(google/cpc) 31세션과 네이버 광고(naver/cpc) 29세션이 섞인 값이었고, 기기로만 자르면 두 네트워크가 한 칸에 들어갑니다.

같은 표를 유입 소스로 한 번 더 자르자 「모바일」 칸 안이 정반대로 갈라졌습니다. 구글 광고의 모바일 방문은 이탈률 93%, 평균 체류 1초였습니다. 네이버 광고의 모바일 방문은 이탈률 27%, 평균 체류 51초였고, 이 27%는 소스와 기기를 곱한 네 칸 가운데 가장 낮은 이탈률이었습니다.

같은 모바일 안에서 유입 소스별 이탈률 막대그래프 — 구글 광고 모바일 93%, 네이버 광고 모바일 27%
같은 「모바일」 칸을 유입 소스로 다시 자른 이탈률

정반대 신호 두 개의 평균이 「모바일이 붕괴했다」로 읽힌 것입니다. 무너진 칸은 모바일 전체가 아니라 구글 광고의 모바일 한 칸뿐이었고, 그 칸은 구글 광고 실제 클릭 8건 위에 서 있었습니다.

왜 평균이 이렇게 쉽게 속일까요. 한 칸에 서로 다른 집단이 들어 있으면, 그 칸의 평균은 어느 집단의 모습도 닮지 않습니다. 구글 모바일 방문은 93%가 참여 없이 끝나고 평균 체류가 1초이며, 네이버 모바일 방문은 이탈이 27%에 그치고 평균 51초를 머뭅니다. 둘을 합친 「모바일 9초」는 실제로 존재하는 방문자 누구의 행동도 아닌, 두 집단의 비율이 만든 숫자였습니다.

이 결과는 H1을 크게 약하게 만들었습니다. 같은 페이지를 같은 모바일로 열었는데 네이버에서 온 방문자는 정상적으로 머물렀기 때문입니다. 페이지가 모바일에서 느려서 떠난다면 네이버 방문자도 똑같이 떠나야 합니다. 통계 용어로 말하면 기기라는 변수가 광고 네트워크라는 변수와 교락(confounding)돼 있었습니다.

돌아보면 판독을 의심할 단서는 처음부터 표 안에 있었습니다. 유료 검색이라는 채널 이름 하나 아래에 성격이 다른 두 광고 네트워크가 있었고, 두 네트워크는 광고를 보여 주는 지면도, 검색어도, 클릭 뒤 사용자의 기대도 다릅니다. 채널 이름이 하나라고 해서 방문자 집단이 하나인 것은 아니었습니다.

어떻게 고쳤나: 소스 안에서만 기기를 비교하도록 도구를 바꿨다

조치는 모바일 화면이 아니라 측정 도구에 들어갔습니다. 판독이 한 번의 일회성 쿼리로 만들어졌다가 한 번의 일회성 쿼리로 뒤집히는 구조 자체가 문제였기 때문입니다.

첫째, 재현 도구에 `--by-source` 옵션을 넣어 유입 소스를 축으로 추가했습니다(PR #208). 코드 변경은 한 줄입니다.

차원 하나를 더했을 뿐인데 결과 표의 모양이 바뀝니다. 전에는 착지 페이지와 기기 두 칸으로 한 줄이 정해졌다면, 이제는 같은 페이지·같은 기기라도 구글에서 왔는지 네이버에서 왔는지에 따라 줄이 나뉩니다. 평균을 내기 전에 집단을 먼저 가르는 셈입니다. 옵션을 켜지 않으면 예전과 같은 표가 나오므로 기존 리포트는 그대로 두었습니다.

GA4 조회 차원에 sessionSource를 추가하는 코드 변경 전후
착지×기기 조회에 유입 소스 축을 더한 변경

둘째, 기기 대조는 같은 소스 안에서만 하도록 운영 규칙으로 못 박았습니다. 구글 모바일은 구글 데스크톱과, 네이버 모바일은 네이버 데스크톱과만 비교합니다. 두 네트워크를 합친 「모바일 평균」은 더 이상 판정 근거로 쓰지 않습니다.

셋째, 표본이 작은 칸에서는 비율 판정을 막았습니다. 조회 스크립트에 최소 세션 기준을 30으로 두고, 이보다 적은 칸에는 비율을 판정에 쓰지 말라는 경고를 붙입니다. 구글 모바일 칸의 실제 클릭 8건은 이 기준에 한참 못 미칩니다.

  • 기각한 대안 1 — 표를 그대로 두고 모바일 성능 수리에 착수: 잘못된 축을 고치는 일이라 v1~v3의 실패를 다시 만든다고 판단했습니다.
  • 기기 대조를 소스 안에서만 하도록 규칙화: 두 네트워크를 합친 평균은 판정 근거에서 뺐습니다.
  • 기각한 대안 2 — 소스 분리를 일회성 쿼리로만 확인하고 도구화 생략: 애초에 문제가 일회성 쿼리의 재현 불가였으므로 같은 구멍을 다시 파는 셈이었습니다.

재현 도구와 소스 분리 옵션 두 가지가 저장소에 들어가면서, 이 절단은 누구든 명령 한 줄로 다시 뽑을 수 있게 바뀌었습니다. 다음에 같은 표를 보는 사람은 판독을 믿을지 말지를 기억이 아니라 실행 결과로 정할 수 있습니다.

재측정 결과: 무너진 칸은 하나였고, 그 칸의 세션 절반은 광고 클릭이 아니었다

소스로 다시 잰 결과를 한 표에 모으면 다음과 같습니다. 같은 30일 창, 같은 속성, 같은 내부 방문 제외 조건입니다.

기기로만 자른 판독과 소스로 다시 자른 결과 비교 표
처음 판독과 재측정 결과

재측정에서 하나가 더 드러났습니다. 구글 광고 계정이 센 클릭은 19건인데, GA4가 google/cpc로 센 세션은 31건으로 클릭의 1.6배였습니다. 모바일만 보면 클릭 8건에 세션 15건으로 1.9배였습니다.

GA4는 광고 클릭 뒤 일정 기간 안에 다시 들어온 방문에도 같은 광고 소스를 붙입니다. 그렇다면 「유료 세션」 가운데 상당수는 광고를 누른 방문이 아니라 재방문 조각이라는 추론이 가능합니다. 그런 조각 세션은 1초 만에 이탈로 찍혀 구글 모바일 칸의 지표를 한 번 더 끌어내렸을 가능성이 큽니다. 다만 이 부분은 세션 하나하나의 귀속을 확인하지 못해 추정으로 남겨 둡니다.

가설 목록도 다시 정렬했습니다. H1(콜드 로드)은 약해졌고, H2(검색어와 첫 화면 제목 불일치)만 살아남았습니다. 오히려 H2는 검사가 쉬워졌습니다. 구글 쪽 대조 대상이 「홈페이지제작」과 「AI챗봇」 광고그룹 2개로 줄었기 때문입니다. 클릭이 광고에서 페이지까지 전달되는 경로가 네트워크마다 다르다는 새 가설 H5는 아직 검증하지 못했습니다.

살아남은 H2를 검사하는 방법도 구체적으로 정했습니다. 광고그룹 2개 각각에 대해, 광고가 노출된 검색어와 착지 페이지 첫 화면의 제목을 나란히 놓고 방문자가 기대한 내용과 실제로 본 내용이 맞는지 봅니다. 대조 대상이 적어져서 사람이 직접 읽어 판단할 수 있는 크기가 됐습니다.

남는 한계: 실제 클릭 8건으로는 어떤 지표도 판정할 수 없다

이번 수정으로 틀린 판독 하나를 걷어냈지만, 결론을 내릴 수 있게 된 것은 아닙니다. 문제의 칸이 구글 광고 실제 클릭 8건 위에 서 있다는 사실은 그대로이고, 8건 위에서는 어떤 지표를 새로 설계해도 판정할 만한 표본이 나오지 않습니다.

숫자로 보면 간격이 분명합니다. 우리가 비율 판정에 쓰기로 한 최소 기준은 한 칸에 30세션인데, 구글 모바일 칸의 실제 클릭은 8건입니다. 여기에 GA4가 붙인 재방문 조각까지 더해 15세션으로 부풀어 있어도 기준의 절반에 못 미칩니다. 이 상태에서 이탈률 93%는 「구글 모바일 방문자가 떠난다」는 결론이 아니라 「아직 모른다」는 표시로 읽어야 합니다.

그래서 다음 순서가 바뀌었습니다. 지표를 더 정교하게 다듬는 일보다 유입량을 확보하는 일이 먼저라는 점이 분명해졌습니다. 조사 시점에 유료 유입이 7일 연속 0이었다는 사실도 같은 방향을 가리킵니다.

  • 세션별 귀속 미확인 — 클릭 19건과 세션 31건의 차이가 전부 재방문 조각인지는 확인하지 못했습니다.
  • H5 미검증 — 네트워크별 클릭 전달 경로 차이는 아직 가설입니다.
  • GA4 표시값의 함정 — 이후 같은 도구에서 채널 이름을 「organic」처럼 줄여 조회하면 0세션이 나오고 정확한 이름 「Organic Search」로 조회해야 108세션이 나오는 문제가 8월 13일에 드러나, 짧은 이름을 정확한 값으로 바꾸는 매핑을 추가했습니다.
  • 걷어내지 못한 잡음 — 이후 9월 1일 www 호스트 30일 403세션에서 봇으로 보이는 착지를 빼자 280세션까지 줄었고(123세션, 31%), `(not set)` 착지 28건 중 9건은 필터로도 빠지지 않아 잡음으로 안고 갑니다.

같은 조회 도구에서 뒤이어 나온 「섞임」은 무엇이었나?

기기와 광고 네트워크의 섞임을 고친 뒤에도, 같은 조회 스크립트에서 비슷한 모양의 섞임이 두 번 더 나왔습니다. 두 건 모두 숫자는 틀리지 않았는데 무엇을 합쳐 셌는지가 보이지 않아서 생긴 문제였습니다.

첫째는 호스트 섞임입니다. 8월 11일 점검에서 확인해 보니, 호스트 필터를 가진 조회 함수는 이번에 만든 착지×기기 함수 하나뿐이었고 나머지 조회 함수 6개는 필터가 없었습니다. 같은 GA4 속성에 노무·세무 등 다른 서비스의 서브도메인이 함께 붙어 있어서, 그 방문이 www와 ai 숫자에 조용히 섞여 들어오고 있었습니다. 조치는 필터를 함수마다 심는 대신 모든 조회가 거쳐 가는 한 곳에 거는 것이었습니다. 그래야 새 조회 함수가 생겨도 자동으로 같은 범위가 걸립니다. 필터를 걸지 않고 전체를 합산할 때는 리포트 머리에 「전체 합산」이라고 쓰고 호스트별 세션 수를 같이 내도록 바꿨습니다.

둘째는 읽는 쪽에서 지워지던 이벤트입니다. 버튼 클릭을 뜻하는 `click` 이벤트가 GA4에는 정상으로 도착하고 있었는데(30일 50건, ai 30건·www 7건 등), 리포트 코드가 기본 이벤트를 걸러 내는 접두사 목록에 `click`을 넣어 두는 바람에 모든 리포트에서 0건처럼 보였습니다. 콘텐츠에서 서비스 페이지로 넘어가는 다리를 계측해 놓고 정작 집계 단계에서 버리고 있었던 셈입니다. 명시적인 허용 목록에 넣어 다시 살렸습니다.

세 건의 공통점은 같습니다. 지표 하나가 여러 집단이나 여러 출처를 합친 결과인데, 리포트가 그 사실을 말해 주지 않았습니다. 그래서 지금 이 조회 스크립트는 섞어서 셀 때 섞었다고 먼저 밝히는 쪽으로 바뀌었습니다.

GA4 이탈률을 기기별로 비교하기 전에 무엇을 확인해야 하나?

이번 일에서 얻은 점검 순서를 정리합니다. 숫자가 맞는지보다 그 숫자가 어떤 칸들을 섞어서 나왔는지를 먼저 봐야 합니다.

  • 기간 — 조회 창이 30일이어도 광고가 실제로 돈 날이 3일일 수 있습니다. 광고 계정의 클릭 발생일부터 확인합니다.
  • 소스 — 기기로 자르기 전에 유입 소스(sessionSource)로 먼저 자릅니다. 기기 비교는 같은 소스 안에서만 합니다.
  • 표본 — 칸마다 세션 수를 같이 봅니다. 30세션 미만 칸의 비율은 판정에 쓰지 않습니다.
  • 클릭과 세션 — 광고 계정의 클릭 수와 GA4의 광고 소스 세션 수를 대조합니다. 세션이 클릭보다 많으면 재방문 조각이 섞였다는 신호입니다.
  • 계정 상태 — 광고 최종 URL은 코드 상수가 아니라 광고 API로 현재 값을 조회합니다.
  • 내부 방문 — 회사 사람이 확인차 들어온 방문을 뺀 뒤에 셉니다.

검색광고의 전환율을 어떤 기준으로 봐야 하는지는 검색광고 전환율 기준선 글에 따로 정리했습니다. 광고 유입이 들어오는데 문의로 이어지지 않아 원인을 함께 짚어 보고 싶다면 상담으로 알려 주세요.

같은 방식으로 우리 업무를 재보고 싶다면

이 글에 쓴 계측·판정·자동화는 저희가 실제로 매일 돌리는 방식 그대로입니다. 어떤 작업을 자동화할 수 있고 어디를 사람이 잡아야 하는지, 현재 업무 흐름을 놓고 같이 짚어 드립니다.

새 글이 올라오면 알려 드립니다

이런 계측 기록을 새로 쓰면 메일로 한 통 보냅니다. 광고나 판촉은 보내지 않습니다.