"스크립트를 넣는 것과, 그 스크립트가 뭘 하고 있는지 아는 것은 다른 일이다."
Next.js App Router 루트 레이아웃에 gtag.js 스크립트를 추가하고, 라우트가 바뀔 때마다 page_view 이벤트를 다시 보내는 컴포넌트를 만들었습니다. 흔히 쓰는 방식대로 짠 코드였습니다:
인라인 스크립트가 로드되자마자 gtag('config', ...)를 호출해서 첫 페이지 방문을 자동으로 전송 별도의 클라이언트 컴포넌트가 useEffect로 페이지 경로 변화를 감지해서 매번 gtag('config', ...)를 다시 호출
각각은 정상적인 코드였는데, 둘을 합쳐놓고 보니 문제가 생겼습니다. 사이트에 처음 들어왔을 때, 인라인 스크립트가 자동으로 한 번 보내고, 그 직후 컴포넌트가 마운트되면서 똑같은 페이지에 대해 또 한 번 보냈습니다. 즉 첫 방문이 항상 2건으로 집계됐습니다. 계획을 다시 검토하는 단계에서 코드를 실제로 실행해보기 전에 이 구조를 짚어보다가 발견했습니다.
인라인 스크립트에서 gtag('config', ..., { send_page_view: false })로 자동 전송을 꺼버렸습니다. 그리고 모든 page_view 전송을 useEffect 쪽 한 곳에서만 일관되게 발생시키도록 정리했습니다. 이렇게 하면 첫 방문이든 페이지 이동이든 항상 정확히 한 번만 이벤트가 나갑니다. 추가로, admin 경로(/admin/*)는 본인만 쓰는 페이지라 방문자 유입 통계에 포함시킬 이유가 없어서 같은 컴포넌트에서 함께 제외 처리했습니다.
콘텐츠를 여러 채널에 발행하기 시작하면서, 어느 채널이 실제로 방문자를 데려오는지 보려고 GA4를 붙였습니다. 스크립트만 넣으면 끝날 줄 알았는데, 첫 페이지 로드마다 방문 기록이 두 번씩 잡히는 버그가 있었습니다. 원인과 해결 과정을 기록합니다.
이 버그를 잡은 방식은 "스크립트 두 개 중 하나를 지우는" 게 아니라 "역할을 하나씩만 갖게 하는" 것이었습니다.
인라인 스크립트의 역할: gtag 객체 초기화만 담당, 자동 전송은 끔 useEffect 컴포넌트의 역할: 실제 page_view 전송을 전담
두 코드가 같은 일(page_view 전송)을 각자 할 수 있게 만들어두면, 나중에 둘 중 하나만 수정해도 다른 쪽이 여전히 같은 일을 하고 있어서 다시 중복이 생길 수 있습니다. 그래서 "전송"이라는 책임을 한쪽에만 남기는 방식으로 재설계했습니다.
Next.js App Router에 GA4를 붙일 때 반복적으로 나올 수 있는 함정과 대응입니다.
App Router는 클라이언트 사이드 라우팅이라, GA4의 기본 자동 page_view만으로는 라우트 변경을 못 잡습니다 — 별도 트래킹 컴포넌트가 필요합니다. 그 별도 컴포넌트를 추가하는 순간, 초기 로드 시 자동 전송과 겹치는 문제가 거의 항상 발생합니다 — send_page_view: false를 먼저 걸어두는 게 기본값이어야 합니다. 검증은 브라우저 개발자도구 Network 탭에서 collect 요청 개수를 직접 세어보는 것으로 충분합니다 — GA4 콘솔의 실시간 리포트만 보면 집계 지연 때문에 판단이 늦어질 수 있습니다. 관리자 페이지처럼 본인만 쓰는 경로는 애초에 이 단계에서 같이 제외 처리해두는 게, 나중에 "내부 트래픽 필터"를 따로 설정하는 것보다 간단합니다.
관련 프로젝트
프로젝트 개요 기획자(PM)로서 AI를 활용한 업무 자동화(AX) 사례를 문서화하는 개인 포트폴리오 사이트입니다. 국내 기업의 AX 기획자/AI PM 직군으로의 커리어 전환을 목표로, 사이트 자체가 채용 지원용 스토리텔링 자료로 실사용되고 있습니다. 핵심 패턴: 업무 병목 발견 → AI로 워크플로우 재설계 → Before/After·근거·재현 가능성 문서화 전체 서사는 '실행하지 않은 아이디어 나열'이 아니라, 하나의 검증된 자동화 패턴과 도메인별 확장 블루프린트를 갖춘 재사용 가능한 엔진을 보유하는 방향으로 수렴하고 있습니다. 기술 스택 Next.js 16, Tailwind CSS v4, TypeScript Supabase (개발/운영 환경 분리), Vercel, Resend Claude API 기반 기술 자문 및 프롬프트 설계 보안 아키텍처 관리자 CRUD는 세션 기반 인증과 RLS(Row Level Security)로 보호 공개 읽기 전용 경로는 별도의 익명 클라이언트로 분리 서비스 롤 키는 애플리케이션 코드에서 완전히 배제 권한 검증은 마이그레이션 파일 및 대시보드 검토를 통해서만 진행 작업 방식 (에이전틱 IDE 협업 워크플로우) AI 에이전트 기반 IDE가 파일/버전관리 작업을 담당하고, LLM은 기술 자문 및 프롬프트 작성 역할을 수행하는 협업 구조를 운영합니다. AI 자문 → 에이전트용 프롬프트 작성 → 실행자가 붙여넣기 → 계획 확인 → 자문 검토 → 실행 승인 → 로그 검토 → 직접 클릭 테스트 → 명시적 지시 시에만 커밋 통제 규칙 실행 전 계획 검토 필수 요약 보고가 아닌 직접 클릭 테스트로 검증 개발 환경 우선 적용 후 명시적 승인 하에 운영 환경 반영 커밋은 명시적 지시가 있을 때만 수행 DB 자격증명 직접 연결 및 범위 외 삭제 금지 브랜치 전략: 개발 브랜치에서 작업 후 운영 브랜치는 배포 전용으로 직접 커밋 금지, 병합 전 변경 범위 검증 현재 진행 상황 고객사 미팅 브리핑 자동화 사례: 1단계 완료, 개발/운영 환경 배포 및 실데이터 테스트 완료 범위 변경 감지 자동화 사례: 상태 확인 진행 중 리스크 조기 경보 자동화 사례: 운영 환경 배포 완료 보안 사고 관련 포스트: 'AI 리스크 거버넌스 사례'로 재구성 예정 AI 활용 관련 콘텐츠 시리즈 초안 작성 완료, 게시 미확정 핵심 학습 원칙 보고가 아닌 검증: 버그·보안 이슈는 항상 직접 로그/브라우저 검토를 통해서만 발견되며, 에이전트의 자체 결과 요약을 그대로 신뢰하지 않음 보안 사고의 콘텐츠화: 실제 발생한 권한 관련 이슈 사례를 케이스 스터디로 재구성하여 콘텐츠화 커밋 의미 분리: 기능 단위/변경 유형별로 항상 분리 커밋 자동화하지 않는 것도 스토리: 저사용 빈도 기능을 의도적으로 고도화하지 않은 결정 자체를 AX 스토리텔링 소재로 활용