React SPA에서 Google Analytics 4로 페이지 뷰 추적

발행: (2026년 2월 23일 오전 11:30 GMT+9)
5 분 소요
원문: Dev.to

Source: Dev.to

Cover image for Tracking Page Views in a React SPA with Google Analytics 4

표준 HTML 웹사이트에 Google Analytics (GA4)를 추가하는 것은 간단합니다: 추적 스니펫을 <head>에 붙여넣기만 하면 끝납니다. 사용자가 링크를 클릭할 때마다 브라우저는 새로운 HTML 페이지를 가져오고, GA는 페이지 뷰를 기록합니다.

하지만 React, Vite, 그리고 React Router를 사용해 싱글 페이지 애플리케이션 (SPA)을 구축하고 있다면, 이 기본 동작이 깨집니다.

React SPA에서는 링크를 클릭해도 페이지가 새로 고침되지 않습니다. React는 단순히 기존 컴포넌트를 언마운트하고 새 컴포넌트를 마운트하면서 브라우저의 URL 히스토리를 조작합니다. 페이지가 실제로 새로 고침되지 않기 때문에 Google Analytics는 새로운 URL을 기록하지 못하고, 분석 결과에서는 사용자가 영원히 홈 페이지에 머물고 있는 것처럼 보입니다.

아래는 제가 포트폴리오 사이트에 적용한 단계별 해결 방법입니다.

1. 환경 설정

트래킹 ID를 소스 코드에 하드코딩하지 마세요. GA 대시보드에서 확인할 수 있는 측정 ID(G-XXXXXXXXXX로 시작하는 경우가 대부분)를 .env 파일에 추가합니다:

VITE_GA_TRACKING_ID=G-**********

2. 초기 HTML 스니펫 (수정됨)

index.html에 기본 Google Analytics 추적 코드를 포함하되, 자동 page_view 추적을 비활성화하여 중복 집계를 방지합니다.

<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){ dataLayer.push(arguments); }
  gtag('js', new Date());

  // IMPORTANT: Disable the default page_view tracking here!
  gtag('config', '%VITE_GA_TRACKING_ID%', { send_page_view: false });
</script>

Vite에 대한 참고: %VITE_GA_TRACKING_ID%는 빌드 시 환경 변수를 주입합니다.

3. 라우트 리스너 컴포넌트 만들기

react-router-domuseLocation을 사용하여 URL 변경을 감지하는 가볍고 보이지 않는 컴포넌트를 생성합니다.

src/components/Analytics.tsx

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

// Extend window object for TypeScript
declare global {
  interface Window {
    gtag: (...args: any[]) => void;
  }
}

export const Analytics = () => {
  const location = useLocation();

  useEffect(() => {
    const GA_MEASUREMENT_ID = import.meta.env.VITE_GA_TRACKING_ID;

    if (GA_MEASUREMENT_ID && typeof window.gtag === 'function') {
      window.gtag('config', GA_MEASUREMENT_ID, {
        page_path: location.pathname + location.search,
      });
    }
  }, [location]);

  return null; // Invisible component
};

4. 라우터에 연결하기

<Analytics /><BrowserRouter> 내부에, <Routes> 블록 외부에 마운트합니다.

src/index.tsx

import { Analytics } from './components/Analytics';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { HelmetProvider } from 'react-helmet-async';
import { Suspense } from 'react';
import ScrollToTop from './components/ScrollToTop';
import LoadingFallback from './components/LoadingFallback';
import HomePage from './pages/HomePage';
import AboutPage from './pages/AboutPage';
// ...other imports

const App = () => {
  return (
    <HelmetProvider>
      <BrowserRouter>
        {/* Place the listener here! */}
        <Analytics />

        <ScrollToTop />
        <Suspense fallback={<LoadingFallback />}>
          <Routes>
            <Route path="/" element={<HomePage />} />
            <Route path="/about" element={<AboutPage />} />
            {/* ... other routes */}
          </Routes>
        </Suspense>
      </BrowserRouter>
    </HelmetProvider>
  );
};

export default App;

5. 결과

  • 사용자가 vicentereyes.org에 접속하면 GA 스크립트가 로드됩니다.
  • React가 부팅되고 라우터가 마운트되며 <Analytics /> 컴포넌트가 /에 대한 페이지 뷰 이벤트를 발생시킵니다.
  • 이후 탐색(예: “Projects” 클릭) 시 URL이 업데이트되고 <Analytics /> 내부의 useEffect가 트리거되어 /projects에 대한 깔끔한 페이지 뷰 이벤트를 전송합니다.

이렇게 하면 무거운 외부 라이브러리 없이 정확한 SPA 추적이 가능합니다.

0 조회
Back to Blog

관련 글

더 보기 »