스택이 다른 사이트 셋에 같은 추적 코드 심기

한 도메인 아래 사이트 넷을 굴리는데 스택이 제각각이다.

ttakochan.com             Astro 정적 (MPA)
longshort.ttakochan.com   Next.js 16 App Router
counter.ttakochan.com     Vite + React SPA
map.ttakochan.com         Next.js

여기에 Google 태그 관리자를 컨테이너 하나로 심었다. 스니펫은 똑같은데 넣는 자리와 실패 방식이 스택마다 달랐다.

만들 것

각 사이트에서 컨테이너가 로드되고 dataLayer 에 초기화 이벤트가 한 번만 쌓이는 상태.

전제

  • 컨테이너 ID 하나 (GTM-XXXXXXX)
  • 각 저장소의 최상위 레이아웃 파일을 고칠 수 있을 것
  • 하위 도메인끼리 같은 상위 도메인 아래에 있을 것

마지막 조건이 중요하다. 같은 도메인 아래면 쿠키가 자동으로 공유돼서 교차 도메인 설정이 필요 없다.

Astro 정적

src/layouts/Base.astro 처럼 모든 페이지를 감싸는 레이아웃 하나에만 넣으면 된다.

<head>
  <meta charset="utf-8" />
  <script is:inline>
    (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','GTM-XXXXXXX');
  </script>
</head>

is:inline 이 없으면 안 된다. Astro 는 인라인 스크립트를 기본으로 번들에 넣고 별도 .js 파일로 빼낸다. 그러면 <head> 최상단 실행 보장이 깨진다.

확인

빌드 결과에 스크립트가 인라인으로 남아 있어야 한다.

grep -o 'src="[^"]*gtm[^"]*\.js"' dist/index.html

여기서 뭔가 잡혔다면 is:inline 을 빠뜨려 외부 파일로 빠진 거다.

Next.js App Router

App Router 에서는 레이아웃에 <head> 를 직접 못 쓴다. <body> 첫머리에 넣는다.

const GTM_ID = "GTM-XXXXXXX";
const gtmInit = `(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','${GTM_ID}');`;

export default function Layout({ children }) {
  return (
    <html lang="ko">
      <body>
        <script dangerouslySetInnerHTML={{ __html: gtmInit }} />
        <noscript>
          <iframe
            src={`https://www.googletagmanager.com/ns.html?id=${GTM_ID}`}
            height="0" width="0"
            style={{ display: "none", visibility: "hidden" }}
          />
        </noscript>
        {children}
      </body>
    </html>
  );
}

페인트 전에 돌아야 하는 스크립트가 이미 있다면

내 경우 테마 깜빡임을 막는 스크립트가 <body> 첫 줄에 있었다. 그건 GTM 보다 앞에 둔다. GTM 은 몇십 밀리초 늦어도 상관없지만 테마는 늦으면 화면이 번쩍인다.

확인

빌드 산출물에서 세는데 여기서 한 번 헷갈린다.

grep -o 'GTM-XXXXXXX' .next/server/app/ko/index.html | wc -l

Next.js 는 HTML 과 RSC 페이로드 양쪽에 마크업을 넣는다. 그래서 2배로 나온다. 실제로 실행되는 건 한 번이다. RSC 페이로드는 데이터지 실행되는 HTML 이 아니다.

DOM 에서 확인해야 정확하다.

[...document.scripts].filter(s => s.src.includes('gtm.js')).length   // 1 이어야 정상
(window.dataLayer||[]).filter(o => o && o['gtm.start']).length        // 1 이어야 정상

404 페이지는 빠진다

_not-found_global-error[locale] 레이아웃 밖이라 스크립트가 안 들어간다. 내 경우 111개 페이지 중 109개만 적용됐다. 404 유입을 추적하려면 루트 레이아웃을 따로 만들어야 한다.

Vite + React SPA

index.html 하나가 전부라 제일 단순하다. 원본 스니펫을 고칠 게 없다.

<head> 끝에 스크립트를, <body> 첫 줄에 noscript iframe 을 넣는다. 빌드하면 dist/index.html 에 그대로 들어간다. 번들러가 건드리지 않는다.

CSP 가 있으면 헤더도 열어야 한다

이게 제일 오래 걸렸다. 스니펫은 HTML 에 멀쩡히 들어 있고 스크립트 태그도 DOM 에 있는데 아무 일도 안 일어났다.

performance.getEntriesByType('resource')
  .filter(e => e.name.includes('googletagmanager'))
// [{ transferSize: 0, duration: 0 }]

요청은 나갔는데 아무것도 못 받았다. 콘솔에 답이 있었다.

Loading the script 'https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXX'
violates the following Content Security Policy directive: "script-src ..."

CSP 를 쓰는 사이트는 헤더를 같이 열어야 한다. 지시어가 넷이고 하나씩 열면 다음 게 막힌다. 이건 분량이 있어서 따로 쓴다.

마지막 확인

세 사이트에서 같은 걸 찍어 값이 전부 같아야 한다.

JSON.stringify({
  container: Object.keys(window.google_tag_manager||{}).includes('GTM-XXXXXXX'),
  gtm_start: (window.dataLayer||[]).filter(o=>o&&o['gtm.start']).length,
  scripts: [...document.scripts].filter(s=>s.src.includes('gtm.js')).length,
  noscript: [...document.querySelectorAll('noscript')].filter(n=>n.innerHTML.includes('ns.html')).length,
})
container: true, gtm_start: 1, scripts: 1, noscript: 1

콘솔 오류가 0 건이어야 한다. 탭을 새로 열고 확인해라. 같은 탭에서 여러 번 새로고침하면 이전 로드의 오류 로그가 남아 있어서 판단이 안 된다. 나는 이것 때문에 고친 뒤에도 안 고쳐진 줄 알았다.

안 되면

컨테이너는 뜨는데 아무 태그도 안 돈다. 정상이다. 컨테이너는 빈 상자라 안에 태그를 만들어 게시해야 돈다.

게시했는데도 안 돈다. 컨테이너 스크립트를 직접 받아 확인한다.

curl -s "https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXX" | grep -c 'G-XXXXXXXXXX'

0 이면 게시가 안 된 거다. 미리보기에서 잘 보이는 것과 게시는 별개다.

한 사이트만 안 된다. 그 사이트에 CSP 가 있는지 본다.

하지 말 것

애드센스를 태그 관리자로 넣지 마라. Google 이 권장하지 않는다. 비동기 로드라 광고 표시가 늦어진다. 광고 스크립트는 코드에 직접 두는 게 맞다.

컨테이너를 여러 사이트가 공유하면 트리거에 호스트 조건을 걸어라. 안 걸면 한 사이트용 이벤트 태그가 네 곳에서 다 발동한다.

Page Hostname  equals  map.ttakochan.com

트리거가 스무 개쯤 됐을 때는 되돌리기 어렵다. 처음부터 규칙으로 잡는 편이 낫다.

← 블로그 목록