Develop

Next.js App Router에서 Google Analytics 4 설정하는 방법 — 페이지뷰와 이벤트 추적까지

UA 종료 후 GA4로 전환한 환경에서 Next.js App Router에 Google Analytics 4를 연동하는 방법을 정리했습니다. next/script 로드, usePathname 페이지뷰 추적, 커스텀 이벤트까지 다룹니다.

Next.jsGoogle AnalyticsGA4App RouterSEO
Next.js App Router에 Google Analytics 4 스크립트를 next/script로 로드하고 GA4 실시간 보고서에서 방문자가 확인되는 화면
  • ·Google Analytics 4는 2023년 7월 Universal Analytics 종료 후 표준이 됨
  • ·GA4 측정 ID 형식: G-XXXXXXXXXX
  • ·Next.js SPA에서 클라이언트 이동 시 pageview 이벤트는 수동으로 전송해야 함
  • ·NEXT_PUBLIC_ 접두사가 있는 환경변수만 클라이언트 사이드에서 접근 가능
구글이 갑작스럽게 UA 서비스를 셧다운하고 GA4를 표준으로 밀어붙였을 때, 제가 기존 프로젝트에서 즐겨 쓰던 라이브러리들이 작동하지 않아 큰 충격을 받았습니다. 어쩔 수 없이 공식 레퍼런스 문서를 밑바닥부터 파헤치며 `next/script`로 gtag 초기화를 직접 짜고, `usePathname`을 결합한 페이지 전환 감지 모듈을 작성해 올렸습니다. 삽질하는 도중에는 머리가 꽤나 아팠지만, 일단 세팅을 마치고 실시간 대시보드 화면에 접속자의 국가와 클릭 이벤트들이 실시간으로 초록색 그래프를 그리며 반응하는 걸 봤을 때의 그 성취감은 정말 대단했습니다.

1. UA 종료와 GA4 패러다임의 변화

UA와 Google Analytics 4(GA4)의 차이 및 Next.js 연동의 핵심

구글이 오랜 기간 사용되던 Universal Analytics(UA)를 단종하고 Google Analytics 4(GA4)를 강제 도입했을 때 분석 마케팅 업계뿐만 아니라 프론트엔드 개발자들도 엄청나게 삽질을 해야 했습니다. UA는 단순히 웹페이지가 로드될 때 '페이지뷰(PV)'라는 고정된 데이터를 전송하는 게 기본 모델이었지만, GA4는 마우스 클릭, 드래그, 스크롤 등 사용자의 모든 행위를 동등한 '이벤트(Event)' 형태로 수집하도록 완전히 설계가 개편되었기 때문입니다. 이 변화는 Single Page Application(SPA) 아키텍처를 지향하는 Next.js 개발 환경에 직접적인 영향을 주었습니다. 예전에는 웹페이지 전환 시 브라우저가 새로고침되지 않아도 구버전 라이브러리들이 뒤에서 억지로 페이지뷰를 전송해 주곤 했지만, GA4에서는 우리가 직접 세세하게 이벤트를 발송해 주어야 정밀한 측정이 가능해졌습니다. 게다가 인터넷에 널려 있는 구버전 블로그 글들은 여전히 예전 UA 방식의 설정 코드를 복사해 놓은 경우가 대다수라, 이를 그대로 복붙했다가 데이터가 하나도 잡히지 않아 애를 먹었던 경험이 한 번쯤은 있을 것입니다. 따라서 Next.js의 렌더링 생명주기에 맞춘 올바른 이벤트 전송 파이프라인 구축이 필수적입니다.

Next.js App Router에서 분석 스크립트를 로드하는 적절한 전략

Next.js App Router 환경에서 GA4 추적 스크립트를 올리는 가장 표준적인 방법은 바로 프레임워크 자체에서 제공하는 next/script 패키지의 <Script> 컴포넌트를 이용하는 것입니다. 간혹 일반 HTML 태그인 <script>를 루트 레이아웃에 직접 던져 넣는 초보적인 실수를 하곤 하는데, 그러면 Next.js의 하이드레이션 과정과 충돌이 나서 스크립트 로드 타이밍을 제어하기가 매우 까다로워집니다. next/scriptstrategy 속성을 제공하는데, GA4 스크립트의 경우 페이지 상호작용이 가능해진 직후 비동기로 로드되는 afterInteractive 전략을 적용하는 것이 최적입니다. 이를 너무 일찍 받아오게 beforeInteractive로 설정해 버리면 분석 스크립트 파일 크기 때문에 첫 페이지 렌더링 성능 지표인 LCP(Largest Contentful Paint)가 나빠질 수 있습니다. 한편 Next.js 14.0.2부터는 공식 최적화 패키지인 @next/third-parties가 릴리즈되면서 <GoogleAnalytics gaId="G-..." /> 컴포넌트 하나로 처리가 가능해지기도 했습니다. 하지만 서비스 커스텀 분석 로직이나 복잡한 동적 동의서 처리 등이 얽혀 있다면 여전히 스크립트 컴포넌트를 이용해 정교하게 초기화 타이밍을 제어하는 직접 구현 방식이 장기적인 유지보수 측면에서 더 유리합니다.

2. GA4 연동 및 스크립트 작성

환경 변수를 활용해 layout.tsx에 GA4 측정 ID 연동하기

GA4를 개설하면 발급받을 수 있는 측정 ID(G-로 시작하는 식별값)는 보안상 노출되어도 큰 피해가 가는 키워드는 아니지만, 개발 서버(development)와 실 배포 서버(production)의 분석 데이터를 분리하기 위해서라도 반드시 환경변수로 관리해 주는 것이 정석입니다. Next.js에서는 클라이언트 사이드 즉, 사용자 브라우저에서 실행되는 JS 번들에서도 접근할 수 있어야 하므로 환경변수명 앞에 무조건 NEXT_PUBLIC_ 접두사를 붙여 주어야 합니다. .env 파일에 NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX 형태로 정의한 뒤, app/layout.tsx 파일 내에서 process.env.NEXT_PUBLIC_GA_ID로 가져와 사용합니다. 스크립트 컴포넌트를 넣을 때 주의할 점은 개발 모드에서는 로딩을 건너뛰도록 NODE_ENV === 'production' 검사 식을 걸어주는 팁입니다. 이 처리를 깜빡하고 넘어갔다가는 로컬에서 코드 몇 줄 고치고 새로고침을 누를 때마다 본인의 로컬 접속 이력이 실제 GA4 라이브 리포트에 고스란히 쌓여 통계 수치가 엄청나게 왜곡되는 대참사를 겪을 수 있습니다. 조건문을 적절히 엮어 프로덕션 배포 서버에서만 로드되도록 안전장치를 만들어 주는 것을 절대로 잊지 맙시다.

// app/layout.tsx
import Script from 'next/script'

const GA_ID = process.env.NEXT_PUBLIC_GA_ID

export default function RootLayout({ children }) {
  return (
    <html lang="ko">
      <body>
        {GA_ID && (
          <>
            <Script
              strategy="afterInteractive"
              src={`https://www.googletagmanager.com/gtag/js?id=${GA_ID}`}
            />
            <Script id="ga-init" strategy="afterInteractive">
              {`window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','${GA_ID}');`}
            </Script>
          </>
        )}
        {children}
      </body>
    </html>
  )
}

App Router 환경에서 누락 없는 페이지뷰 이벤트 추적 구현하기

많은 신입 개발자들이 범하는 대표적인 실수가 '메인 layout.tsx에 스크립트만 잘 임포트해 두면 페이지 이동할 때 알아서 페이지뷰가 전송되겠지' 하고 넘어가는 것입니다. 전통적인 MPA와 달리, Next.js App Router는 페이지를 바꿀 때 실제 서버에 새 HTML 페이지를 요청하는 것이 아니라 필요한 JS 번들만 가져와 클라이언트 단에서 브라우저 주소창(history state)만 갈아끼우는 SPA 렌더링을 수행합니다. 이 때문에 Nginx나 외부에서 스크립트 로드 자체는 한 번만 발생하므로, 사용자가 블로그 목록에서 상세 글로 주소를 바꾸더라도 GA4는 주소 이동을 전혀 감지하지 못합니다. 이 문제를 깔끔하게 해결하기 위해서는 클라이언트 컴포넌트 내부에서 usePathname 훅을 구독하고, pathname이 달라질 때마다 리액트의 useEffect 안에서 수동으로 gtag('event', 'page_view', ...) 이벤트를 트리거해 주어야 합니다. 나도 초기에 배포하고 며칠간 메인 페이지만 트래픽이 집중되고 개별 글의 조회수가 0으로 찍혀서 헤맸던 뼈아픈 실수가 있었는데, 이 페이지뷰 디듀프/수동 추적 컴포넌트를 연동하고 나서야 비로소 각 포스트별 방문 트래픽 통계가 서치 콘솔과 일치하게 제대로 집계되는 성과를 보았습니다.

'use client'
import { usePathname } from 'next/navigation'
import { useEffect } from 'react'

export function GAPageview({ gaId }: { gaId: string }) {
  const pathname = usePathname()
  useEffect(() => {
    window.gtag?.('event', 'page_view', { page_path: pathname })
  }, [pathname])
  return null
}

3. 커스텀 이벤트 설계 및 데이터 검증

사용자 액션을 상세하게 수집할 GA4 커스텀 이벤트 코딩하기

단순히 어떤 페이지에 들어왔는지를 넘어서, 사용자가 글 끝까지 도달했는지(스크롤 100%), 깃허브 링크 버튼을 눌렀는지, 혹은 다크모드 스위치를 켰는지 등의 구체적인 동작을 잡아내고 싶다면 커스텀 이벤트(Custom Event) 발송 코드를 구현해야 합니다. 자바스크립트 전역 window 객체에 바인딩된 gtag 함수를 직접 찔러 넣어 주는 방식인데, 협업 환경이나 대형 프로젝트에서는 무작정 난사하기보다는 lib/analytics.ts 같은 헬퍼 모듈을 따로 뽑아 타입 세이프(Type-safe)하게 포장해서 쓰는 편이 훨씬 안정적입니다. 예를 들어 trackClickEvent(buttonName: string)처럼 파라미터 규격을 미리 명문화해 두면 나중에 GA4 대시보드에서 분석가가 통계를 뽑아볼 때 이벤트명이 뒤죽박죽 꼬이는 불상사를 원천 차단할 수 있습니다. 깃허브나 외부 레퍼런스 페이지로 넘어가는 아웃바운드 링크 클릭을 추적할 때는, 클릭 직후 리다이렉트가 일어나면서 분석 데이터가 구글 서버로 넘어가기 전에 페이지가 닫히는 비동기 처리 타이밍 이슈가 발생하곤 합니다. 이럴 때는 이벤트 콜백(transport: 'beacon') 옵션을 활성화하여 브라우저가 백그라운드에서 안전하게 전송을 보장하도록 가이드해 주어야 누락 없는 수집이 가능합니다.

실시간 보고서와 디버거 툴로 Google Analytics 데이터 정상 전송 확인하기

GA4는 수집된 정식 보고서가 통계 테이블에 컴파일되어 올라오는 데 최소 24시간에서 48시간까지 대기 지연이 생깁니다. 그래서 방금 심은 코드가 정상적으로 발송되는지 알아보기 위해서는 구글 애널리틱스 콘솔 안의 '실시간(Real-Time)' 보고서 탭을 적극 이용해야 합니다. 배포를 진행한 직후 스마트폰이나 외부 네트워크 브라우저로 접속을 해보고, 수 초 내에 실시간 대시보드의 활성 사용자 카드가 깜빡이며 새로운 이벤트 로그에 page_view나 우리가 정의한 커스텀 이벤트가 찍히는지 지켜보는 짜릿함이 있습니다. 만약 10분이 지나도 그래프가 꿈쩍도 안 한다면 십중팔구 브라우저에 켜져 있는 애드블록(Adblock)이나 uBlock Origin 같은 광고 차단 확장 플러그인이 googletagmanager.com 도메인 요청을 패킷 단에서 블락하고 있는 상황입니다. 테스트 환경에서는 반드시 분석 차단 예외 필터를 해제하거나 시크릿 창을 켜서 검사하는 게 좋습니다. 또한 브라우저 콘솔에서 즉각적인 전송 payload를 모니터링해 주는 'Google Analytics Debugger' 크롬 익스텐션을 설치해 쓰면 개발 서버 환경에서도 디버그 뷰를 통해 클릭 하나하나가 JSON 규격대로 제대로 꽂히는지 실시간 디버깅이 가능해 훨씬 쾌적해집니다.

자주 묻는 질문

next/script 컴포넌트의 strategy 옵션에서 afterInteractive와 lazyOnload 중 왜 afterInteractive를 선호하나요?+

lazyOnload는 브라우저가 완전히 한가한 상태(idle)가 된 후에야 로드되는데, 사용자가 접속하자마자 이탈(Bounce)해 버리면 분석 스크립트 자체가 실행되기도 전에 나가게 되므로 초기 유입수가 통째로 유실됩니다. 반면 afterInteractive는 페이지 요소들이 정상 배치되어 최소한의 스크롤이나 클릭 상호작용이 가능해질 때 바로 실행되므로 누락률이 훨씬 낮아 권장됩니다.

Next.js 정적 배포(output: 'export')를 통해 CDN에 단순 HTML로 올린 사이트도 GA4를 쓸 수 있나요?+

네, 아무 문제 없이 동작합니다. Google Analytics 4 스크립트는 클라이언트 브라우저 단에서 순수 자바스크립트로 실행되는 도구이므로, 빌드 시점의 서버 렌더링 상태와 무관하게 브라우저에 코드가 도달하면 실시간으로 트래픽을 수집해 갈 수 있습니다.

관련 글