🍪

ReactでCookie同意バナーとGA4を実装する

に公開

はじめに

GDPR(EU一般データ保護規則)やCCPA(カリフォルニア州消費者プライバシー法)など、世界各地でプライバシー保護に関する法規制が強化されています。これらの規制では、WebサイトがCookieを使用してユーザーをトラッキングする前に、明確な同意を得ることが求められています。
Google Analytics 4(GA4)を導入しているサイトも例外ではありません。ユーザーの同意なくトラッキングを開始することは、法的リスクを伴う可能性があります。

Cookie同意の実装方法

Cookie同意の実装には、大きく分けて2つの方法があります。

  1. CMP(Consent Management Platform)を利用する
  • OneTrust、Cookiebot、Osanoなどのサービス
  • 導入が簡単で法的要件への準拠が保証される
  • 月額費用がかかる場合が多い
  1. 自前で実装する
  • コストがかからない
  • サイトのデザインに完全にマッチさせられる
  • 必要最小限の機能だけを実装できる
  • ⚠️ 法的要件への準拠は自己責任となる
  • ⚠️ 法改正への対応やメンテナンスが必要

この記事では、自前でCookie同意バナーを実装し、GA4と適切に連携させる方法を紹介します。特に、Reactアプリケーションで陥りやすい実行順序の問題とその解決方法について詳しく解説します。

Cookie同意バナーとは

Cookie同意バナー(Cookie Consent Banner)は、Webサイトを訪問したユーザーに対して、Cookieの使用について同意を求めるUI要素です。ユーザーは「同意する」「拒否する」などのボタンをクリックすることで、Cookieの利用を許可するかどうかを選択できます。
実装例

Google Analytics 4(GA4)の基本

GA4とは

Google Analytics 4(GA4)は、Googleが提供するWebサイトのアクセス解析ツールです。訪問者数、ページビュー、滞在時間などを測定できます。
Google Analyticsロゴ

GA4とCookie同意バナーの関係

GA4では、Consent Mode v2という仕組みを使って、ユーザーの同意状態をトラッキングシステムに伝えます。これにより、同意が得られるまでトラッキングを行わず、プライバシーを保護することができます。Consent Mode v2では以下の4つの項目を制御します。

  • analytics_storage: アナリティクス用Cookie
  • ad_storage: 広告用Cookie
  • ad_user_data: 広告用のユーザーデータ
  • ad_personalization: パーソナライズ広告

gtag.jsとは

GA4を使うには、WebサイトにGoogleが提供するgtag.jsというJavaScriptライブラリを読み込む必要があります。

<!-- gtag.jsのスクリプトを読み込む -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX" ></script>
<script>
  // gtagという関数が使えるようになる
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
</script>

このスクリプトを読み込むと、gtag()という関数がグローバルに使えるようになります。この関数を使って、GA4にイベントやユーザーの同意状態を送信します。

gtag()関数の基本的な使い方

gtag()関数は、第1引数にコマンド名、第2引数以降にパラメータを渡します。

  1. 同意状態の設定(consent)
// デフォルトで拒否状態に設定
gtag('consent', 'default', {
  analytics_storage: 'denied',  // アナリティクス用Cookieを拒否
  ad_storage: 'denied'          // 広告用Cookieを拒否
})
// ユーザーが同意したら許可状態に更新
gtag('consent', 'update', {
  analytics_storage: 'granted', // 許可に変更
  ad_storage: 'granted'
})
  1. トラッキングの開始(config)
// 自分のGA4測定IDを指定してトラッキング開始
gtag('config', 'G-XXXXXXXXXX')

gtagを利用したConsent Mode v2の基本的な流れ

それでは、ページ読み込みからトラッキング開始までの全体の流れを図で確認しましょう。

フローのポイント

  1. 赤色(denied状態): デフォルトで拒否状態から始まる
  2. 緑色(granted状態): ユーザーが同意した場合のみ
  3. 青色(config実行): 必ず consent update の後に実行される

実行順序の重要性(consent default → update → config)

Consent Modeの実装で最も重要なのは、以下の順序を守ることです。

  1. gtag('consent', 'default', { ...denied }) // デフォルトで拒否
  2. gtag('consent', 'update', { ...granted }) // ユーザーの同意後に更新
  3. gtag('config', 'G-XXXXXXXXXX') // トラッキング開始

もしこの順序が守られないと

  • ユーザーの同意を得る前にトラッキングが開始される
  • プライバシー規制に違反する可能性がある
  • GA4で正しい同意状態が記録されない

という問題が起こります。

Reactでの実装における課題

前述のConsent Modeの流れを実装する際、Reactアプリケーションでは特有の課題があります。

課題1: useEffectの実行順序が保証されない

ReactのuseEffectは非同期で実行され、複数のコンポーネント間での実行順序は保証されていません
例えば、以下のような実装を考えてみます。

// 親コンポーネント
useEffect(() => {
  gtag('config', 'G-XXXXXXXXXX')  // トラッキング開始
}, [])
// Cookie同意バナーコンポーネント
useEffect(() => {
  gtag('consent', 'update', {...}) // 同意状態を更新
}, [consentState])

このコードでは、どちらのuseEffectが先に実行されるか分かりません。タイミングによってはgtag('config')が先に実行され、ユーザーの同意状態が反映されない可能性があります。

課題2: 同意状態の永続化

ユーザーが選択した同意状態は、ページをリロードしても保持される必要があります。2回目以降の訪問時にはバナーを表示せず、保存された状態をそのままGA4に反映します。
これを実現するにはLocalStorageを使いますが、Reactのライフサイクルとの連携に工夫が必要です。

解決策

これらの課題を解決するため、この記事では以下の手法を使います。

  1. コールバックパターン: Cookie同意バナーから親コンポーネントへコールバック関数を渡し、consent updateの直後にconfigを実行することで順序を保証
  2. LocalStorage: 同意状態を永続化し、リロード時にも状態を復元
  3. 条件付きレンダリング: 同意済みの場合はバナーを表示しない

次のセクションで、これらを組み込んだ完全な実装コードを紹介します。

実装コード

Cookie同意バナーコンポーネントの実装

まず、Cookie同意バナーコンポーネントを実装します。
このコンポーネントでは以下の流れで処理を行います。

  1. バナーを表示してユーザーに同意/拒否を選択してもらう
  2. 選択結果をLocalStorageに保存して次回以降はバナーを表示しない
  3. gtag('consent', 'update')で同意状態を更新する
  4. 親コンポーネントに通知してトラッキングを開始させる
CookieConsent.tsx
import { useEffect, useState } from 'react'

const ConsentState = {
  Granted: 'granted',
  Denied: 'denied'
} as const
export type ConsentState = (typeof ConsentState)[keyof typeof ConsentState]

const updateConsent = (state: ConsentState) => {
  window.gtag?.('consent', 'update', {
    analytics_storage: state,
    ad_storage: state,
    ad_user_data: state,
    ad_personalization: state
  })
}

type Props = {
  onConsentChange?: (state: ConsentState) => void
}

export const CookieConsent = ({ onConsentChange }: Props) => {
  // バナーを表示するかどうか
  const [showBanner, setShowBanner] = useState(false)

  useEffect(() => {
    const stored = localStorage.getItem(STORAGE_KEY) as ConsentState | null
    if (stored) {
      updateConsent(stored)
      onConsentChange?.(stored)
      return
    }
    setShowBanner(true)
  }, [])

  const handleConsent = (state: ConsentState) => {
    localStorage.setItem(STORAGE_KEY, state)
    updateConsent(state)
    onConsentChange?.(state)
    setShowBanner(false)
  }

  // すでに選択済みの場合はバナーを表示しない
  if (!showBanner) return null

  return (
    <div>
      <div>
        <p>
          このサイトはCookieを使用して、サイトの利用状況を分析します。
          <a href="/privacy-policy">
            詳細はこちら
          </a>
        </p>
        <div>
          <button
            onClick={() => handleConsent(ConsentState.Denied)}
          >
            拒否する
          </button>
          <button
            onClick={() => handleConsent(ConsentState.Granted)}
          >
            同意する
          </button>
        </div>
      </div>
    </div>
  )
}

ルートコンポーネントの実装

次に、アプリケーションのルートコンポーネントを実装します。
このコンポーネントでは以下の2つの処理を行います。

  • GA4の初期設定
    • デフォルトで「拒否」状態に設定する
    • gtag.jsライブラリを動的に読み込む
  • Cookie同意バナーとの連携
    • ユーザーが選択したらコールバックで通知を受け取る
    • 通知を受けたらトラッキングを開始する

以下はReact Router v7でのルートコンポーネント(ssr:false, prerender:true)の実装例です。

root.tsx
import { CookieConsent, ConsentState } from './CookieConsent'

const GA_MEASUREMENT_ID = 'G-XXXXXXXXXX' // 自分のIDに置き換える

export default function Root() {

  // Cookie同意後にGA4を初期化
  const handleConsentStateChanged = (_state: ConsentState) => {
    window.gtag?.('config', GA_MEASUREMENT_ID)
  }

  return (
    <html>
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        {/* gtag.jsの読み込み */}
        <script
          async
          src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`}
          />
        {/* consent デフォルト設定と gtag.js の初期化 */}
        <script
          dangerouslySetInnerHTML={{
            __html: `
              // 1. dataLayerの初期化
              window.dataLayer = window.dataLayer || [];
              function gtag(){dataLayer.push(arguments);}
              // 2. 最初にconsent defaultを設定
              gtag('consent', 'default', {
                analytics_storage: 'denied',
                ad_storage: 'denied',
                ad_user_data: 'denied',
                ad_personalization: 'denied'
              });
              // 3. gtagの初期化
              gtag('js', new Date());
            `
          }}
        />
      </head>
      <body>
        {/* アプリのコンテンツ */}
        <div id="app">
          {/* ... */}
        </div>
        {/* Cookie同意バナー */}
        <CookieConsent 
          onConsentChange={handleConsentStateChanged} 
        />
      </body>
    </html>
  )
}

Next.jsでの実装例はこちらです。

Next.jsでの実装例(クリックで展開)

Next.js(App Router)の場合は、app/layout.tsxと専用のClient Componentで実装します。

1. ConsentManagerコンポーネント(Client Component)

ConsentManager.tsx
'use client'

import { CookieConsent, ConsentState } from './CookieConsent'

const GA_MEASUREMENT_ID = 'G-XXXXXXXXXX'

export function ConsentManager() {
  // Cookie同意後にGA4を初期化
  const handleConsentStateChanged = (_state: ConsentState) => {
    window.gtag?.('config', GA_MEASUREMENT_ID)
  }

  return <CookieConsent onConsentChange={handleConsentStateChanged} />
}

2. ルートレイアウト(Server Component)

app/layout.tsx
import { Script } from 'next/script'
import { ConsentManager } from './ConsentManager'

const GA_MEASUREMENT_ID = 'G-XXXXXXXXXX'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="ja">
      <head>
        <Script
          id="gtag-consent-default"
          strategy="beforeInteractive"
          dangerouslySetInnerHTML={{
            __html: `
              window.dataLayer = window.dataLayer || [];
              function gtag(){dataLayer.push(arguments);}

              gtag('consent', 'default', {
                analytics_storage: 'denied',
                ad_storage: 'denied',
                ad_user_data: 'denied',
                ad_personalization: 'denied'
              });

              gtag('js', new Date());
            `
          }}
        />
        <Script
          src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`}
          strategy="afterInteractive"
        />
      </head>
      <body>
        {children}
        <ConsentManager />
      </body>
    </html>
  )
}

ポイント

  • strategy="beforeInteractive" で consent default を最初に実行
  • usePathnameとuseSearchParamsはClient Componentでのみ使用可能
  • ConsentManagerでコールバック処理とCookieConsentをまとめて管理

型定義の追加

最後に、TypeScriptでwindow.gtagを使用するための型定義を追加します。このファイルをプロジェクトのルートまたはsrcディレクトリに配置してください。

global.d.ts
declare global {
  interface Window {
    dataLayer: any[]
    gtag: (
      command: 'consent' | 'config' | 'event' | 'js' | 'set',
      ...args: any[]
    ) => void
  }
}

export {}

動作検証

実装が完了したら、正しく動作しているか確認しましょう。ここでは2つの方法を紹介します。

window.dataLayerを使った確認

dataLayerは、gtagが内部的に使用するイベントの配列です。このオブジェクトの状態を見ることで、各コマンドが正しい順序で実行されているかを確認できます。
ブラウザの開発者ツールを開き、コンソールで以下を実行してください。

console.log(window.dataLayer)

以下のような配列が表示されれば、正しく実装されています。

[
  {
    "0": "consent",
    "1": "default",
    "2": { "analytics_storage": "denied", ... } // デフォルトは拒否
  },
  {
    "0": "consent",
    "1": "update",
    "2": { "analytics_storage": "granted", ... } // ユーザーが同意
  },
  {
    "0": "config",
    "1": "G-XXXXXXX"
  }
]

ポイントは、consent updateがconfigより前に記録されていることです。この順序が逆になっている場合は、コールバックの実装を見直してください。

Network tabでの確認

実際にGA4へデータが送信されているかは、ブラウザの開発者ツールのNetworkタブで確認できます。

  1. 開発者ツールを開き、Networkタブを選択
  2. Preserve logにチェックを入れる(ページ遷移後もログを保持するため)
  3. フィルタに collect と入力して絞り込む

この状態でCookie同意バナーの「同意する」ボタンをクリックしてください。https://www.google-analytics.com/g/collect?... へのリクエストが表示されれば、トラッキングが正常に動作しています。

さらに詳しく確認したい場合は、リクエストURLのクエリパラメータを見てください。

  • &gcs=G111 → 全て許可(granted)
  • &gcs=G110 → 一部拒否

「拒否する」をクリックした場合は、トラッキングデータは送信されず、最小限の計測のみ行われます。

まとめ

この記事では、ReactアプリケーションでCookie同意バナーとGA4を正しく連携させる方法を紹介しました。

実装のポイント

GA4のスクリプトを読み込む際に、gtag('consent', 'default', {...})でデフォルトの同意状態を設定します。これはスクリプトタグ内で同期的に実行されるため、ページ読み込み時に確実に設定されます。

2. コールバックパターンで実行順序を保証

ReactのuseEffectは実行順序が保証されません。そこで、Cookie同意バナーコンポーネントから親コンポーネントへコールバック関数を渡し、consent updateの直後にconfigを実行することで、確実に正しい順序でGA4を初期化できます。

3. LocalStorageで同意状態を永続化

ユーザーの選択をLocalStorageに保存することで、2回目以降の訪問時にはバナーを表示せず、保存された同意状態を自動的に反映します。

最後に

この記事で紹介した実装は、私の 個人サイトで実際に使用しています。動作を確認したい方は、ぜひアクセスしてみてください。
GA4のConsent Modeは、プライバシー保護とアクセス解析を両立させるための重要な仕組みです。この記事が、皆さんの実装の参考になれば幸いです。

参考リンク

Discussion