구글 애널리틱스와 태그 관리자 — EditImg.online에 붙이기

운영하는 사이트(EditImg.online)에 방문자 통계를 붙였습니다. 검색해보면 "구글 애널리틱스"와 "구글 태그 관리자"가 늘 같이 나오는데, 이름이 비슷하고 로고도 닮아서 둘 중 뭘 설치해야 하는지부터 헷갈립니다. 실제로 붙여보니 헷갈릴 만한 이유가 있었습니다.

정리하면서, 제 사이트에 실제로 어떻게 들어갔는지까지 같이 적어둡니다.


1. 둘은 경쟁 관계가 아니라 역할이 다릅니다

한 줄로 요약하면 이렇습니다.

애널리틱스는 데이터가 쌓이는 창고, 태그 관리자는 스크립트를 꽂았다 뺐다 하는 리모컨입니다.

구글 애널리틱스(GA4) — 창고

방문자 수, 어떤 페이지를 봤는지, 어디서 유입됐는지가 저장되고 보고서로 보이는 쪽입니다. 우리가 실제로 눈으로 들여다볼 화면이 여기입니다. 현재 버전이 4세대라서 GA4라고 부릅니다.

구글 태그 관리자(GTM) — 리모컨

GTM은 데이터를 하나도 저장하지 않습니다. 사이트에 어떤 추적 스크립트를 넣을지 관리만 합니다. 여기서 "추적 스크립트 하나"를 부르는 이름이 바로 태그입니다.

  • GA4로 조회를 보내는 스크립트 → GA4 태그
  • 광고 전환을 알려주는 스크립트 → 전환 태그
  • 이른바 "픽셀" → 역시 태그

"태그"는 특정 제품 이름이 아니라 페이지에 심는 작은 스크립트 조각의 총칭입니다. 이 단어가 제품명처럼 보여서 혼란이 생깁니다.

그래서 뭘 설치하나

둘 다 씁니다. 사이트 코드에는 GTM만 넣고, GA4는 GTM 안에서 태그로 붙입니다.

사이트 코드   →   GTM 컨테이너   →   GA4 태그   →   GA4 보고서
(스니펫 1개)      (리모컨)          (실제 측정)     (창고)

2. GA4를 직접 넣을까, GTM을 거칠까

GA4는 사이트에 직접 넣을 수도 있습니다. 발급받으면 이런 코드를 줍니다.

<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-PT31CCVKSK"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'G-PT31CCVKSK');
</script>

저도 이걸 그냥 붙일 생각이었는데, GTM을 이미 깔아둔 상태라면 넣지 않는 게 맞습니다. 가장 큰 이유는 하나입니다.

조회가 두 번 집계됩니다. 스니펫을 직접 넣고 GTM 안에도 GA4 태그를 만들면 같은 방문이 중복으로 잡힙니다. 방문자 수가 실제의 두 배로 보이고, 이탈률이나 세션당 페이지수 같은 지표까지 전부 오염됩니다. 나중에 원인 찾기가 꽤 번거로운 종류의 문제입니다. 둘 중 하나만 골라야 합니다.

그럼 어느 쪽을 고를까요. 흔히 "GTM을 거치면 요청이 하나로 줄어든다"고 설명하는데, 확인해보니 사실이 아니었습니다. GTM 컨테이너 파일 안에 gtag/js 참조가 들어 있습니다. 즉 GTM 경로도 결국 gtag 스크립트를 받아옵니다.

curl -s 'https://www.googletagmanager.com/gtm.js?id=GTM-P3GP84X7' | grep -c 'gtag/js'
# 2  ← 컨테이너가 gtag를 불러온다

실제 전송량을 재보면 이렇습니다(브라우저가 받는 압축 상태 기준).

파일 실전송 크기
gtm.js (GTM 컨테이너) 약 118 KB
gtag/js (GA4 로더) 약 165 KB

순수 전송량만 보면 gtag를 직접 넣는 쪽이 가볍습니다. GTM을 쓰면 컨테이너가 얹히니까요. 그러니 GTM을 고르는 근거는 속도가 아닙니다.

진짜 이득은 배포가 필요 없어진다는 점입니다. 측정 ID가 코드에 없으니, 앞으로 태그를 추가하거나 바꿀 때 커밋·푸시·재배포 없이 GTM에서 게시 버튼만 누르면 됩니다. 나중에 광고 전환 추적이나 다른 도구를 붙일 때도 코드를 다시 안 건드립니다. 약간의 무게를 내고 운영 편의를 사는 거래라고 보면 정확합니다.

정적 사이트라 배포가 1~2분이면 끝나는 제 경우엔 어느 쪽이든 괜찮았지만, 앞으로 붙일 게 더 있다고 보고 GTM으로 갔습니다.

3. 실제로 사이트에 들어간 코드

제 사이트는 Astro로 만든 정적 사이트이고 7개 언어 78페이지입니다. 모든 페이지가 공통 레이아웃(src/layouts/Base.astro) 하나를 쓰기 때문에 여기 한 곳만 고치면 전 페이지에 반영됩니다.

컨테이너 ID는 상수로

// Google 태그 관리자 컨테이너 ID — 애널리틱스는 GTM 안에서 태그로 붙인다.
// 공개 값이고 바뀌지 않으므로 저장소에 둔다.
// 환경변수가 있으면 그쪽이 우선한다(컨테이너를 새로 만들었을 때).
const GTM_ID = import.meta.env.PUBLIC_GTM_ID || 'GTM-P3GP84X7';

컨테이너 ID는 페이지 소스만 열면 누구나 보이는 공개 값이고 바뀌지도 않습니다. 그래서 환경변수에만 두지 않고 코드에 상수로 뒀습니다. 환경변수에만 두면 호스팅을 옮기거나 사이트를 다시 만들 때 조용히 사라져서, 통계가 끊긴 걸 한참 뒤에 알게 됩니다. 대신 환경변수가 있으면 그쪽이 이기게 해서 컨테이너를 새로 만들 때 코드를 안 고쳐도 되게 했습니다.

스니펫 배치 — 여기서 판단이 하나 필요했습니다

구글 공식 안내는 "<head> 안 가능한 한 위쪽"입니다. 그런데 제 사이트에는 특이한 사정이 있습니다. 방문자가 기본 언어(영어) 페이지에 오면, 브라우저 언어를 감지해서 해당 언어 페이지로 한 번 이동시키는 스크립트가 <head>에 있습니다.

// 저장된 언어 설정이 없고 기본 언어 페이지라면, 감지된 언어로 한 번 이동
if (detected && detected !== current) {
  location.replace('/' + detected + ...);
}

GTM을 이 스크립트보다 앞에 두면, 곧 떠날 영어 페이지에서도 조회가 한 번 잡힙니다. 그러면 "유입 언어는 영어가 압도적"이라는 가짜 결론이 나옵니다. 언어별 트래픽을 보고 번역 우선순위를 정하려는 게 통계를 붙인 주 목적인데, 정작 그 숫자가 망가지는 셈입니다.

그래서 GTM 스니펫을 <head> 맨 끝, 리다이렉트 스크립트 뒤에 뒀습니다. 리다이렉트 판정은 동기로 끝나므로 여기까지 실행이 왔다는 건 이동이 없다는 뜻이고, GTM은 어차피 비동기 로드라 실제 속도 손실도 없습니다.

{/* Google 태그 관리자. 언어 감지 리다이렉트 "뒤"에 두는 게 중요하다 — 앞에 두면
    이동하는 페이지에서도 조회가 한 번 잡혀 유입 언어 통계가 부풀려진다. */}
<script is:inline define:vars={{ gtmId: GTM_ID }}>
  (function (w, d, s, l, i) {
    w[l] = w[l] || [];
    w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
    var f = d.getElementsByTagName(s)[0],
      j = d.createElement(s),
      dl = l != 'dataLayer' ? '&l=' + l : '';
    j.async = true;
    j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl;
    f.parentNode.insertBefore(j, f);
  })(window, document, 'script', 'dataLayer', gtmId);
</script>

Astro를 쓸 때 주의점이 두 개 있습니다.

  • is:inline이 필요합니다. 없으면 Astro가 이 스크립트를 번들 파일로 빼내 처리하는데, GTM 스니펫은 페이지에 그대로 박혀 있어야 합니다.
  • define:vars로 ID를 주입합니다. 인라인 스크립트는 프론트매터 변수를 직접 못 읽습니다. define:vars를 쓰면 Astro가 const gtmId = "GTM-P3GP84X7"; 선언을 앞에 붙여줍니다.

자바스크립트가 꺼진 경우

<body> 바로 뒤에 noscript 폴백도 넣습니다.

<body>
  <noscript
    ><iframe
      src={`https://www.googletagmanager.com/ns.html?id=${GTM_ID}`}
      height="0" width="0" style="display:none;visibility:hidden"></iframe
    ></noscript
  >

4. GTM 화면에서 한 일 — 태그, 트리거, 변수

GTM에 처음 들어가면 단어 세 개가 나옵니다. 개념은 단순합니다.

용어 이번에 설정한 값
태그 무엇을 실행할지 Google 태그 (측정 ID G-PT31CCVKSK)
트리거 언제 실행할지 초기화 – 모든 페이지
변수 그때 쓸 값 기본 제공분만 사용

절차는 이렇습니다.

  1. GA4에서 속성을 만들고 데이터 스트림 → 웹을 등록해 측정 ID(G-로 시작)를 받습니다
  2. GTM에서 태그 → 새로 만들기 → Google 태그를 고르고 측정 ID를 넣습니다
  3. 트리거를 초기화 – 모든 페이지로 지정합니다
  4. 미리보기로 발동을 확인하고 제출 → 게시

게시하기 전에는 컨테이너가 비어 있어서 데이터가 한 건도 안 들어옵니다. 저는 이 단계에서 한 번 헤맸습니다. Tag Assistant에 컨테이너는 잡히는데 "이 컨테이너에서 평가된 태그가 없습니다"가 떠서 설치가 실패한 줄 알았는데, 스니펫은 정상이고 컨테이너가 비어 있다는 뜻이었습니다.

초기화 – 모든 페이지를 쓴 이유도 적어둡니다. 비슷한 All Pages 트리거가 따로 있는데, 초기화 트리거가 다른 모든 태그보다 먼저 발동합니다. 나중에 동의 관련 설정을 붙일 때 순서가 어긋나지 않아 안전합니다.

5. 설치 확인 — "됐겠지"로 넘기지 않기

화면에 초록불이 떴다고 끝난 게 아닙니다. 세 군데를 직접 확인했습니다.

하나, 라이브 페이지가 스니펫을 서빙하는지.

curl -s https://editimg.online/ | grep -c 'GTM-P3GP84X7'

둘, 게시된 컨테이너에 측정 ID가 실제로 들어갔는지. 이게 제일 확실한 검증입니다. GTM이 배포하는 파일을 직접 받아서 열어보면 됩니다.

curl -s 'https://www.googletagmanager.com/gtm.js?id=GTM-P3GP84X7' > gtm.js
grep -oE 'G-[A-Z0-9]{8,12}' gtm.js | sort -u

측정 ID가 나오면 게시가 반영된 겁니다. 그리고 이 명령에는 보너스가 있습니다 — 출력이 하나만 나오는지 확인하면 중복 집계 사고를 잡을 수 있습니다. 측정 ID가 두 개 이상 보이면 어딘가에서 GA4가 두 번 붙은 겁니다.

셋, GA4 → 보고서 → 실시간. 여기에 방문이 잡히면 끝입니다. 광고 차단기나 브라우저 추적 방지 기능이 GA4를 막을 수 있으니 시크릿 창으로 확인하는 게 좋습니다.

6. 잊기 쉬운 마무리 — 개인정보처리방침

이게 실제로 제가 놓칠 뻔한 부분입니다. 제 사이트 방침의 "분석" 항목은 원래 이렇게 적혀 있었습니다.

호스팅 제공자가 제공하는 집계 형태의 트래픽 통계만 사용합니다.

GA4를 게시한 순간부터 이 문장은 사실이 아닙니다. GA4는 _ga, _ga_* 쿠키를 설정하는 제3자 도구입니다. 방문자에게 알려야 할 내용이 늘어난 것이고, 방치하면 라이브 사이트에 부정확한 고지가 걸려 있는 상태가 됩니다.

그래서 다음 내용을 넣어 다시 썼습니다.

  • GTM을 통해 불러오는 GA4를 사용한다는 사실
  • 설정되는 쿠키 이름(_ga, _ga_*)과 용도(재방문 구분)
  • IP 주소는 대략적 위치 추정에만 쓰고 저장하지 않는다는 점
  • 브라우저 설정 또는 구글의 애널리틱스 차단 부가기능으로 거부할 수 있다는 점
  • 프로필을 만들지 않고 데이터를 판매하지 않는다는 기존 약속은 유지
  • 최종 수정일 갱신

다국어 사이트라 7개 언어를 다 고쳐야 하나 했는데, 방침 같은 장문은 영어·한국어만 번역해두고 나머지는 영어로 폴백하는 구조였습니다.

/** 키가 없으면 영어로 폴백 (개인정보처리방침 등 장문은 en/ko만 번역되어 있음) */
return DICTS[locale]?.[key] ?? DICTS.en[key] ?? key;

덕분에 두 파일만 고쳐서 7개 언어 전부 반영됐습니다.

7. 이제 볼 수 있게 된 것

붙여놓고 기대하는 건 네 가지입니다.

  • 어떤 도구가 실제로 쓰이는지/resize/, /crop/, /convert/ 중 무엇에 트래픽이 몰리나
  • 어떤 언어로 들어오는지 — 7개 언어 중 어디를 먼저 번역해야 하나
  • 유입 경로 — 구글이냐 네이버냐 직접 입력이냐
  • 가이드 글이 검색 유입을 만드는지

세 번째·네 번째는 다음에 뭘 만들지 정하는 근거가 됩니다. 감으로 정하던 걸 숫자로 정할 수 있게 됐다는 게 이번 작업의 실제 이득입니다.

8. 알아두면 좋은 함정 세 가지

본인 방문도 집계됩니다. 개발하면서 새로고침한 것까지 다 잡힙니다. GA4 → 관리 → 데이터 스트림 → 태그 설정 → 내부 트래픽 정의에서 본인 IP를 제외해야 통계가 깨끗해집니다.

프리뷰 배포도 같은 컨테이너를 씁니다. 저는 Netlify를 쓰는데 브랜치 배포에도 스니펫이 실려서 GA4에 섞입니다. GTM 태그 트리거에 "호스트명이 실제 도메인일 때만" 조건을 걸면 걸러집니다.

AdSense와 GA4는 별개입니다. 광고 수익은 AdSense에서, 트래픽은 GA4에서 따로 봅니다. 나중에 연동하면 GA4 안에서 페이지별 광고 수익까지 같이 볼 수 있습니다.


정리

  • GA4는 창고, GTM은 리모컨. 경쟁 제품이 아니라 역할이 다릅니다
  • "태그"는 제품명이 아니라 페이지에 심는 스크립트 조각의 총칭입니다
  • GTM 경유와 gtag 직접 삽입 중 하나만 고릅니다. 둘 다 넣으면 조회가 두 번 집계됩니다
  • GTM이 더 가볍다는 말은 사실이 아닙니다(컨테이너가 얹힘). GTM의 이득은 배포 없이 태그를 바꿀 수 있다는 운영 편의입니다
  • 컨테이너 ID는 공개 값이니 코드에 두되, 환경변수가 이기게 해두면 안전합니다
  • 스니펫이 리다이렉트보다 앞에 있으면 통계가 왜곡될 수 있습니다
  • 설치 확인은 화면 초록불이 아니라 gtm.js를 직접 받아 측정 ID를 세어보는 쪽이 확실합니다
  • 개인정보처리방침 갱신을 잊지 마세요. GA4를 붙이면 쿠키를 쓰는 제3자 도구가 하나 늘어난 겁니다

다음 단계로는 "내보내기 버튼을 몇 명이 눌렀나" 같은 행동 데이터를 붙일 생각입니다. 그건 GTM 설정만으로는 안 되고 에디터 코드에 dataLayer.push를 심어야 하는데, 리사이즈 실행·ZIP 내보내기·포맷 선택 지점에 넣어두면 어떤 기능이 실제로 쓰이는지 숫자로 보이게 됩니다.

→ 사이트: https://editimg.online