
본 글은 GA4 사용자 행동 데이터 설계 (1)의 후속 글입니다.
1편에서는 사용자 행동 데이터를 어떤 구조로 수집하고, 어떤 기준으로 분석 및 활용할 것인지에 대해 정리하였습니다.
이번 글에서는 해당 설계를 바탕으로, React 환경에서 GA4를 실제로 어떻게 설정하였는지를 정리합니다.
GA4 설정 방식을 직접 선택한 이유
React 환경에서 GA4를 연동할 때, react-ga4와 같은 라이브러리를 사용하는 방식도 고려할 수 있습니다.
다만 본 프로젝트에서는 GA4를 직접 설정하는 방식을 선택하였습니다.
설정 난이도도 높지 않았고, 직접 구성하면 동작 흐름을 더 명확히 파악할 수 있다고 판단했습니다.
GA를 사용하기 위해서는 우선 GA 스크립트를 로드하고, 이벤트를 전송할 수 있는 기본 환경을 구성해야 합니다.
이 섹션에서는 GA가 정상적으로 동작하기 위해 반드시 필요한 최소한의 설정을 중심으로, 초기화 과정과 이벤트 수집 방식을 정리합니다.
initGA 함수GA4 초기화는 gtag.js 스크립트를 로드하고, window.gtag와 dataLayer를 설정하는 과정으로 시작합니다.
declare global {
interface Window {
dataLayer: unknown[];
gtag?: (...args: unknown[]) => void;
}
}
export const initGA = (
googleAnalyticsId: string,
gtagUrl: string = 'https://www.googletagmanager.com/gtag/js',
) => {
if (!googleAnalyticsId) {
console.warn('[GA] Measurement ID missing');
return;
}
// gtag.js 삽입
const script = document.createElement('script');
script.async = true;
script.src = `${gtagUrl}?id=${googleAnalyticsId}`;
document.head.appendChild(script);
// gtag 초기화
window.dataLayer = window.dataLayer || [];
window.gtag = function gtag() {
window.dataLayer.push(arguments);
};
window.gtag('js', new Date());
// SPA 환경에서는 page_view를 직접 제어
window.gtag('config', googleAnalyticsId, {
send_page_view: false,
});
};
page_view 전송을 비활성화하였습니다.GAInitializer 컴포넌트초기화 로직은 앱 실행 시 한 번만 수행되도록 별도의 컴포넌트로 분리하였습니다.
const GAInitializer = () => {
useEffect(() => {
if (!isProduction) return;
initGA(GOOGLE_ANALYTICS_ID);
}, []);
return null;
};
사용자 행동을 GA 이벤트로 기록하기 위해 공통으로 사용할 trackEvent 함수를 정의하였습니다.
GA4에는 Universal Analytics의 category / action / label 개념이 존재하지 않지만, 이벤트를 의미 단위로 묶어 분석하기 위해 커스텀 파라미터 형태로 유사한 구조를 유지하였습니다.
interface TrackEventParams {
category: string;
action: string;
label?: string;
value?: number;
}
export const trackEvent = ({
category,
action,
label,
value,
}: TrackEventParams) => {
if (typeof window.gtag !== 'function') return;
window.gtag('event', action, {
event_category: category,
event_label: label,
value,
});
};
사용 예시는 다음과 같습니다.
<button
onClick={() => {
handleClick();
trackEvent({
category: 'Navigation',
action: '로그인 버튼 클릭',
label: 'Header Login Button',
});
}}
>
로그인
</button>
이와 같은 방식으로 이벤트를 정의하여, 개별 이벤트 자체보다 사용자 행동 흐름 단위로 분석할 수 있도록 구성하였습니다.
앞선 기본 설정만으로도 GA 이벤트 수집은 가능하지만, 해당 상태로는 데이터를 해석하는 데 한계가 존재하였습니다.
이 섹션에서는 수집된 데이터를 페이지 단위, 사용자 단위, 접속 환경 단위로 해석하기 위해 추가로 설정한 내용을 정리합니다.
GA4 연동 이후 확인된 문제 중 하나는 모든 페이지의 title이 동일하게 수집되는 현상이었습니다.
React와 같은 SPA 환경에서는 라우팅이 변경되더라도 document.title이 자동으로 변경되지 않기 때문에, GA 리포트 상에서도 모든 페이지가 동일한 이름으로 집계되고 있었습니다.

이 상태에서는 페이지 단위의 사용자 행동을 해석하기 어렵다고 판단하였습니다.
가장 먼저 적용한 방식은 라우팅 변경을 감지하여 document.title을 직접 변경하는 방법이었습니다.
const TITLE_MAP: Record<string, string> = {
'/': '봄봄 | 오늘의 뉴스레터',
'/storage': '봄봄 | 뉴스레터 보관함',
'/recommend': '봄봄 | 뉴스레터 추천',
'/login': '봄봄 | 로그인',
'/signup': '봄봄 | 회원가입',
};
const PageTitle = () => {
const location = useLocation();
useEffect(() => {
let title = '봄봄';
// 동적 라우트는 prefix 매칭으로 처리
if (location.pathname.startsWith('/articles/')) {
title = '봄봄 | 아티클 상세';
} else {
title = TITLE_MAP[location.pathname] ?? '봄봄';
}
document.title = title;
}, [location.pathname]);
return null;
};
이 방식은 구현이 단순하고, GA에서 페이지별 타이틀이 정상적으로 구분되는 것을 빠르게 확인할 수 있다는 장점이 있었습니다.
다만, 페이지 타이틀 관리 로직이 라우팅 로직과 분리된 상태로 존재하게 되면서, 페이지 메타 정보가 여러 곳에 흩어질 수 있다는 점은 아쉬운 부분이었습니다.
프로젝트에서는 TanStack Router를 사용하고 있으며, 해당 라우터는 라우트 정의 단계에서 문서 헤더를 관리할 수 있는 기능을 제공합니다.
현재 프로젝트에서는 페이지별 타이틀과 메타 정보를 라우트 단위로 정의하는 방식으로 전환하였습니다.
export const Route = createFileRoute('/_bombom/storage')({
head: () => ({
meta: [
{
title: '봄봄 | 뉴스레터 보관함',
},
],
}),
component: () => (
<RequireLogin>
<Storage />
</RequireLogin>
),
});
이 방식의 경우
결과적으로 페이지 단위의 타이틀 관리가 라우팅 구조와 자연스럽게 결합되었고, GA 리포트에서도 페이지별 데이터 해석이 더욱 명확해졌습니다.
프로젝트의 라우터 구성과 구조에 따라 두 방식 중 적절한 방법을 선택하여 적용할 수 있습니다.
페이지별 타이틀 설정을 적용한 결과는 아래 사진과 같습니다.

GA4는 사용자를 단일 기준으로 식별하지 않고, User ID → Device ID → 모델링된 데이터의 우선순위를 기반으로 사용자를 식별합니다.
이 중 User ID는 개발자가 직접 전달해야 하는 값이며, 설정되지 않은 경우 GA는 자동으로 생성한 Device ID를 기준으로 사용자 식별을 수행합니다.
웹 환경에서 Device ID는 브라우저 및 쿠키 기반으로 관리되기 때문에, 동일한 로그인 사용자라도 웹과 모바일 등 서로 다른 기기나 접속 환경에서는 각각 다른 사용자로 인식될 수 있습니다.
이러한 한계를 보완하기 위해, 로그인 기반 서비스에서는 User ID를 설정하여 동일 사용자의 여러 기기·브라우저 접속을 하나의 사용자로 통합하여 분석하는 것이 바람직합니다.
본 프로젝트는 Web / iOS / Android 환경 모두에 배포되어 있어, 동일 사용자를 고유한 사용자로 인식하기 위해 User ID를 설정하였습니다.
서비스 진입 시 사용자 정보를 조회한 뒤, 로그인된 사용자라면 user_id를 GA에 설정합니다.
const user = await queryClient.fetchQuery(queries.userProfile());
if (user) {
window.gtag?.('set', { user_id: user.id });
}
이 방식은 GA 설정을 다시 초기화하지 않고, 현재 측정 컨텍스트에 User ID만 추가하는 형태입니다.
로그아웃 이후에도 이전 User ID가 유지되는 것을 방지하기 위해, 로그아웃 mutation이 성공했을 때 User ID를 초기화합니다.
onSuccess: () => {
window.gtag?.('set', { user_id: null });
window.location.reload();
},
이를 통해 로그아웃 이후 발생하는 이벤트가 이전 사용자와 연결되지 않도록 처리하였습니다.
초기에는 WebView 환경에서 웹에서 필요한 정보만을 userAgent로 설정하였습니다.
구체적으로는,
와 같은 커스텀 정보만을 userAgent에 포함하도록 구성하였습니다.
그러나 이와 같이 커스텀 정보만 userAgent로 설정하였을 경우, GA에서는 해당 접속을 정상적인 기기 정보로 인식하지 못하는 문제가 발생하였습니다.
이는 GA가 기기 카테고리(Desktop / Mobile), OS, 브라우저 여부 등을 userAgent 문자열에 포함된 기본 agent 정보를 기준으로 판단하기 때문입니다.
기본 agent 정보가 누락된 상태에서는, GA가 접속 환경을 올바르게 해석할 수 없었습니다.
이 문제를 해결하기 위해, 커스텀 userAgent를 단독으로 사용하는 대신 기본 agent 정보인 navigator.userAgent를 함께 포함하도록 수정하였습니다.
<WebView
ref={webViewRef}
source={{ uri: ENV.webUrl }}
userAgent={`${navigator.userAgent} ${WEBVIEW_CUSTOM_USER_AGENT}`}
...
/>
이와 같이 구성함으로써, navigator.userAgent를 통해 기기 종류 및 플랫폼 정보가 정상적으로 전달되고 추가한 커스텀 문자열을 통해 앱 버전 및 WebView 접속 여부를 함께 식별할 수 있도록 하였습니다.
이번 글에서는 GA4 사용자 행동 데이터 설계를 바탕으로, React 환경에서 GA를 실제로 어떻게 설정하였는지를 정리하였습니다.
초기 설정부터 페이지 단위, 사용자 단위, 접속 환경 단위로 데이터를 해석하기 위한 설정까지, 실제 서비스 운영 과정에서 고려했던 포인트들을 중심으로 정리하였습니다.
다음 글에서는, 이렇게 수집된 데이터를 GA4 탐색 리포트에서 어떻게 분석하고 활용하였는지를 정리해보려고 합니다.
좋은 글 잘 읽었습니다 :)
기존에도 tanstack router를 활용하고 있으셨던건가요?? 아니면 ga4 설정을 위해 tanstack router를 도입하시게 된건가요??