[Next.js] Google Analytics Data API (GA4) 연동해서 방문자 데이터 가져오기

구현 목표

Google Analytics(GA4)의 Data API를 이용해서

Next.js에서 “일자별 방문자 수”, “운영체제별 방문자 수”, “조회수 상위 페이지” 데이터를 가져오는 API를 구현해 보자.

 

Google Analytics Data API

GA4에서 수집한 통계 데이터를 외부 앱에서 사용할 수 있도록 제공하는 API이다.

이 API를 사용하기 위해서는 서비스 계정을 생성하고, JSON 형태의 키 파일을 발급받아야 한다.

 

서비스 계정 생성

구글 클라우드 콘솔에서 `IAM 및 관리자 > 서비스 계정` 탭에 들어가서 [서비스 계정 만들기] 버튼을 클릭한다.

 

`서비스 계정 ID` 항목만 입력하고 [완료] 버튼을 누르면 간단하게 서비스 계정이 생성된다.

 

JSON 키 파일 발급

새로 생성한 서비스 계정의 이메일을 클릭해서 들어간다.

 

키 탭에서 `키 추가` > [새 키 만들기] 버튼을 클릭한다.

 

`JSON` 유형을 선택해서 비공개 키를 만든다.

 

비공개 키가 생성되었다.

 

JSON 키 파일은 다운로드 폴더에 자동으로 생성된다.

 

구글 애널리틱스 사용자 추가

구글 애널리틱스에 위에서 생성한 서비스 계정의 이메일을 추가해야 한다.

`구글 애널리틱스 > 관리 페이지 > 계정 > 계정 액세스 관리` 페이지에 들어가서

우측 상단에 `플러스 버튼` > [사용자 추가] 버튼을 클릭한다.

 

위에서 생성했던 서비스 계정의 이메일 주소를 입력하고 [추가] 버튼을 누르면 GA에 서비스 계정이 추가된다.

 

GA4 Query Explorer

GA4 Query Explorer라는 사이트에서 원하는 통계를 쿼리 형식으로 미리 확인할 수 있다.

 

먼저 사용하고자 하는 GA4와 연동된 구글 아이디로 로그인한다.

 

“지난 30일간 날짜별 활성 사용자 수”를 가져오는 쿼리를 설정했다.

 

드롭다운 형태로 모든 옵션을 제공해 주기 때문에 간단하게 쿼리를 작성할 수 있다.

 

쿼리 결과를 JSON 또는 테이블 형태로 확인할 수 있고,

이 쿼리를 기반으로 실제 코드에서 API 호출에 활용할 수 있다.

 

프로젝트 구성

프로젝트 세팅

npm i @google-analytics/data

`@google-analytics/data` 라이브러리를 설치한다.

 

`.env` 파일에 프로젝트 속성 ID와 위에서 발급받은 JSON 키 파일을 추가한다.

JSON 키 파일은 `.env` 파일에 붙여넣기하고 줄바꿈을 모두 제거해서 모든 문자열이 한 줄이 되도록 수정한다.

 

9자리의 숫자로 이루어진 속성 ID는 GA4 Query Explorer 또는

`구글 애널리틱스 관리 페이지 > 속성 > 속성 세부정보`에서 확인할 수 있다.

 

구현 코드

Next.js의 API Route를 이용해 `/api/analytics` 엔드포인트를 만들고

Google Analytics Data API를 호출해 한 달간의 데이터를 가져오도록 구성했다.

`runReport()`에 사용되는 쿼리 형식은 GA4 Query Explorer를 참고해 작성하면 된다.

 

import { BetaAnalyticsDataClient } from "@google-analytics/data";

const { GOOGLE_SERVICE_ACCOUNT, GOOGLE_ANALYTICS_PROPERTY_ID } = process.env;

const client = new BetaAnalyticsDataClient({
  credentials: JSON.parse(GOOGLE_SERVICE_ACCOUNT || ""),
});

// 한 달간 방문자 수 일자별 조회
export async function getDailyVisitors() {
  // Google Analytics Data API 호출
  const [response] = await client.runReport({
    property: `properties/${GOOGLE_ANALYTICS_PROPERTY_ID}`,
    dateRanges: [{ startDate: "30daysAgo", endDate: "today" }], // 지난 30일간의 데이터 조회
    metrics: [{ name: "activeUsers" }], // 활성 사용자
    dimensions: [{ name: "date" }], // 날짜별로
  });

  // 데이터 가공
  const result =
    response.rows?.map((row) => ({
      date: row.dimensionValues?.[0]?.value ?? "",
      activeUsers: Number(row.metricValues?.[0]?.value ?? 0),
    })) ?? [];

  return result;
}

// 한 달간 운영체제별 방문자 수 조회
export async function getOSVisitors() {
  const [response] = await client.runReport({
    property: `properties/${GOOGLE_ANALYTICS_PROPERTY_ID}`,
    dateRanges: [{ startDate: "30daysAgo", endDate: "today" }],
    dimensions: [{ name: "operatingSystem" }],
    metrics: [{ name: "activeUsers" }],
  });

  const result =
    response.rows?.map((row) => ({
      os: row.dimensionValues?.[0]?.value ?? "",
      activeUsers: Number(row.metricValues?.[0]?.value ?? 0),
    })) ?? [];

  return result;
}

// 한 달간 조회수 상위 페이지 조회
export async function getTopPages() {
  const [response] = await client.runReport({
    property: `properties/${GOOGLE_ANALYTICS_PROPERTY_ID}`,
    dateRanges: [{ startDate: "30daysAgo", endDate: "today" }],
    dimensions: [{ name: "pageTitle" }],
    metrics: [{ name: "screenPageViews" }],
    orderBys: [{ metric: { metricName: "screenPageViews" }, desc: true }],
    limit: 10,
  });

  const result =
    response.rows?.map((row) => ({
      path: row.dimensionValues?.[0]?.value ?? "",
      pageViews: Number(row.metricValues?.[0]?.value ?? 0),
    })) ?? [];

  return result;
}

데이터 호출은 세 가지 기능으로 나누어 각각 함수로 작성했다.

한 달간 일자별 방문자 수, 한 달간 운영체제별 방문자 수, 한 달간 페이지 조회수 10위를 조회한다.

 

// src\app\api\analytics\route.ts

import {
  getDailyVisitors,
  getOSVisitors,
  getTopPages,
} from "../../lib/analytics";

export async function GET() {
  try {
    const [daily, os, pages] = await Promise.all([
      getDailyVisitors(),
      getOSVisitors(),
      getTopPages(),
    ]);

    return Response.json({
      dailyVisitors: daily,
      osVisitors: os,
      topPages: pages,
    });
  } catch (err) {
    console.error("Analytics summary error:", err);
    return new Response(
      JSON.stringify({ error: "Failed to load analytics summary" }),
      {
        status: 500,
      }
    );
  }
}

위에서 만든 세 개의 함수를 `Promise.all()`로 병렬 호출하여,
하나의 API 요청으로 세 가지 데이터를 동시에 가져올 수 있도록 했다.

물론 이 부분은 화면 기획에 따라 별개의 API로 분리할 수도 있다.

 

import styles from "@/app/styles/Analytics.module.scss";

interface DailyVisitor {
  date: string; // 예: "2025-06-23"
  activeUsers: number;
}

interface OSVisitor {
  os: string; // 예: "iOS", "Windows"
  activeUsers: number;
}

interface TopPage {
  path: string; // 예: "/home", "/detail/abc"
  pageViews: number;
}

interface AnalyticsData {
  dailyVisitors: DailyVisitor[];
  osVisitors: OSVisitor[];
  topPages: TopPage[];
}

export default async function Page() {
  const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/analytics`);
  const data: AnalyticsData = await res.json();

  return (
    <div className={styles.summaryWrapper}>
      <div className={styles.summaryGrid}>
        <section className={styles.summarySection}>
          <h2>📅 일자별 활성 사용자</h2>
          <pre>{JSON.stringify(data.dailyVisitors, null, 2)}</pre>
        </section>

        <section className={styles.summarySection}>
          <h2>💻 운영체제별 활성 사용자</h2>
          <pre>{JSON.stringify(data.osVisitors, null, 2)}</pre>
        </section>

        <section className={styles.summarySection}>
          <h2>📄 조회수 상위 페이지</h2>
          <pre>{JSON.stringify(data.topPages, null, 2)}</pre>
        </section>
      </div>
    </div>
  );
}

API로부터 데이터를 `fetch()`해서 화면에 간단히 보여주는 컴포넌트다.

 

GA4 API 호출 결과를 이렇게 확인할 수 있다.

 

참고 문서