NestJS 일일 리포트에 GA4, GSC 데이터 통합하기
배경: 서버 메트릭만으로는 부족했다
이전 글에서 NestJS Cron과 Grafana Cloud Prometheus API를 연결하여 매일 아침 Slack으로 서버 현황을 받아보는 구조를 만들었습니다. 총 요청 수, 응답 시간, 에러율, 메모리, CPU 같은 서버 메트릭을 한눈에 볼 수 있게 되었는데, 시간이 지나면서 한 가지 아쉬움이 생겼습니다.
서버가 잘 돌아가고 있다는 건 알겠는데, 실제로 사람들이 얼마나 방문하고 있는지, 검색에서 어떤 키워드로 유입되는지는 따로 GA4와 Search Console에 들어가서 확인해야 했습니다. 매일 아침 리포트 하나로 전체 현황을 파악할 수 있다면 좋겠다는 생각이 들었습니다.
구현 목표
기존 일일 리포트에 두 가지 데이터 소스를 추가하는 것이 목표였습니다.
- Google Analytics 4: 어제 사용자 수, 세션, 페이지뷰, 평균 세션 시간, 이탈률
- Google Search Console: 최근 28일 클릭, 노출, CTR, 평균 순위, 인기 검색어 Top 5
핵심 원칙은 이전과 동일합니다. 어떤 데이터 소스가 실패하더라도 나머지는 정상적으로 보고되어야 한다는 것입니다.
Google API 인증: Service Account
GA4와 GSC 모두 Google API를 사용하므로 인증 방식을 통일할 수 있었습니다. Cron 환경에서는 사용자 상호작용 없이 동작해야 하므로 Service Account가 적합합니다.
// google-auth.service.ts
@Injectable()
export class GoogleAuthService {
private readonly keyPath: string | undefined;
private readonly keyJson: string | undefined;
constructor(private readonly configService: ConfigService) {
this.keyPath = this.configService.get<string>('GOOGLE_SERVICE_ACCOUNT_KEY_PATH');
this.keyJson = this.configService.get<string>('GOOGLE_SERVICE_ACCOUNT_KEY_JSON');
}
isConfigured(): boolean {
return !!(this.keyPath || this.keyJson);
}
getAuthClient(scopes: string[]) {
if (this.keyJson) {
const credentials = JSON.parse(this.keyJson);
return new google.auth.GoogleAuth({ credentials, scopes });
}
if (this.keyPath) {
return new google.auth.GoogleAuth({ keyFile: this.keyPath, scopes });
}
throw new Error('Google Service Account not configured');
}
}
로컬에서는 JSON 키 파일 경로(GOOGLE_SERVICE_ACCOUNT_KEY_PATH)를 사용하고, 프로덕션(Coolify)에서는 환경변수에 JSON 문자열(GOOGLE_SERVICE_ACCOUNT_KEY_JSON)을 직접 넣는 방식으로 두 가지 환경을 모두 지원합니다.
GA4 데이터 수집
Google Analytics Data API v1을 사용하여 어제 하루의 주요 지표를 가져옵니다.
// ga4.service.ts
async collectMetrics(): Promise<GA4Metrics> {
const auth = this.googleAuth.getAuthClient([
'https://www.googleapis.com/auth/analytics.readonly',
]);
const analyticsData = google.analyticsdata({ version: 'v1beta', auth });
const response = await analyticsData.properties.runReport({
property: `properties/${this.propertyId}`,
requestBody: {
dateRanges: [{ startDate: 'yesterday', endDate: 'yesterday' }],
metrics: [
{ name: 'totalUsers' },
{ name: 'sessions' },
{ name: 'screenPageViews' },
{ name: 'averageSessionDuration' },
{ name: 'bounceRate' },
],
},
});
const row = response.data.rows?.[0]?.metricValues;
// ... 파싱 후 반환
}
dateRanges에 yesterday를 지정하면 전일 데이터를 깔끔하게 가져올 수 있습니다. GA4는 거의 실시간으로 데이터가 집계되므로 아침 리포트에서 어제 데이터를 보는 데 문제가 없었습니다.
필요한 설정
GA4_PROPERTY_ID: GA4 속성의 숫자 ID (GA4 Admin에서 확인)- Service Account 이메일을 GA4 속성에 뷰어 역할로 추가
GSC 데이터 수집
Search Console API는 GA4와 다른 특성이 있습니다. 데이터 반영에 약 3일의 지연이 있어서, 최근 28일 데이터를 조회할 때 종료일을 3일 전으로 설정해야 합니다.
// gsc.service.ts
async collectMetrics(): Promise<GSCMetrics> {
const auth = this.googleAuth.getAuthClient([
'https://www.googleapis.com/auth/webmasters.readonly',
]);
const searchconsole = google.searchconsole({ version: 'v1', auth });
const endDate = new Date();
endDate.setDate(endDate.getDate() - 3); // 3일 지연 고려
const startDate = new Date(endDate);
startDate.setDate(startDate.getDate() - 27); // 28일 합산
// 전체 요약
const summaryResponse = await searchconsole.searchanalytics.query({
siteUrl: this.siteUrl!,
requestBody: {
startDate: formatDate(startDate),
endDate: formatDate(endDate),
dimensions: [],
rowLimit: 1,
},
});
// 인기 검색어 Top 5
const queriesResponse = await searchconsole.searchanalytics.query({
siteUrl: this.siteUrl!,
requestBody: {
startDate: formatDate(startDate),
endDate: formatDate(endDate),
dimensions: ['query'],
rowLimit: 5,
type: 'web',
},
});
// ... 결과 조합
}
도메인 속성 주의점
구현 과정에서 한 가지 삽질이 있었습니다. GSC에서 사이트를 도메인 속성으로 등록한 경우, API 호출 시 URL을 https://kichang.info가 아닌 sc-domain:kichang.info 형식으로 지정해야 합니다. 처음에 https:// 형식으로 호출했다가 권한 오류가 발생했는데, GSC 설정 화면에서 "도메인 속성"으로 표시되는 것을 확인하고 형식을 바꾸니 정상 동작했습니다.
# .env
GSC_SITE_URL=sc-domain:kichang.info # 도메인 속성인 경우
# GSC_SITE_URL=https://example.com # URL 접두사 속성인 경우
통합 리포트 구성
세 가지 데이터 소스를 Promise.all로 병렬 수집하고, 각 섹션이 독립적으로 동작하도록 구성했습니다.
// daily-report.service.ts
const [serverMetrics, ga4Metrics, gscMetrics] = await Promise.all([
this.grafana.isConfigured() ? this.collectServerMetrics() : null,
this.ga4.isConfigured() ? this.ga4.collectMetrics() : null,
this.gsc.isConfigured() ? this.gsc.collectMetrics() : null,
]);
환경변수가 설정되지 않은 소스는 자동으로 건너뛰고, 특정 소스에서 에러가 발생해도 해당 항목만 "조회 실패"로 표시됩니다. Grafana만 설정된 환경에서도 서버 메트릭만 정상적으로 보고되는 식입니다.
최종 Slack 리포트 형태
📅 2026. 01. 30. 일일 리포트
🖥️ 서버 메트릭
🌐 총 요청: 212건
⚡ 평균 응답: 73ms
⚡ P95 응답: 376ms
❌ 에러율: 0.00%
💾 메모리: 156MB
🔧 CPU: 12.5%
🔄 이벤트루프: 2.1ms
📊 Google Analytics (어제)
👥 사용자: 7명
📱 세션: 7회
📄 페이지뷰: 32회
⏱️ 평균 세션: 3분 8초
↩️ 이탈률: 28.6%
🔍 Search Console (28일)
🖱️ 클릭: 7회
👁️ 노출: 121회
📈 CTR: 5.8%
📍 평균 순위: 8.0
🏆 인기 검색어: nestjs prometheus, coolify, ...
프로젝트 구조
packages/backend/src/daily-report/
├── daily-report.module.ts # 모듈 정의
├── daily-report.service.ts # Cron 스케줄러 + 리포트 포맷팅
├── grafana-prometheus.service.ts # Grafana Cloud PromQL 클라이언트
├── google-auth.service.ts # Google Service Account 인증
├── ga4.service.ts # GA4 Data API 클라이언트
└── gsc.service.ts # GSC API 클라이언트
각 서비스가 단일 책임을 갖도록 분리했습니다. 인증(GoogleAuthService)은 GA4와 GSC가 공유하고, 데이터 수집 서비스는 각자의 API만 담당합니다.
환경변수 정리
# Grafana Cloud (1단계에서 설정 완료)
GRAFANA_CLOUD_PROMETHEUS_URL=https://prometheus-xxx.grafana.net/api/prom
GRAFANA_CLOUD_USER=1234567
GRAFANA_CLOUD_API_TOKEN=glc_xxx
# Google API (2단계 추가)
GOOGLE_SERVICE_ACCOUNT_KEY_PATH=./google-service-account.json # 로컬
# GOOGLE_SERVICE_ACCOUNT_KEY_JSON={"type":"service_account",...} # 프로덕션
GA4_PROPERTY_ID=516093632
GSC_SITE_URL=sc-domain:kichang.info
배운 점
돌이켜 생각해보면, 이번 작업에서 가장 시간이 걸린 부분은 코드 작성이 아니라 인증과 권한 설정이었습니다. Grafana Cloud에서 Service Account Token(glsa_)과 Cloud API Key(glc_)의 차이를 파악하는 데 시간이 걸렸고, GSC의 도메인 속성 URL 형식(sc-domain:)도 직접 삽질해봐야 알 수 있었습니다.
결국 API 연동에서 가장 중요한 것은 인증 방식을 정확히 이해하는 것이라는 결론에 이르렀습니다. API 문서를 읽을 때 엔드포인트와 파라미터만 보는 것이 아니라, 인증 섹션을 가장 먼저, 가장 꼼꼼히 읽어야 한다는 것을 다시 한번 느꼈습니다.
다음 단계로는 수집한 데이터를 Gemini API에 전달하여 AI가 인사이트를 생성해주는 3단계를 구상하고 있습니다. "어제 대비 페이지뷰가 30% 증가했는데, NestJS 관련 검색어 유입이 늘어난 것이 원인으로 보입니다" 같은 분석이 자동으로 추가된다면, 아침 리포트가 훨씬 가치 있어질 것이라는 생각이 듭니다.
관련 글
NestJS Cron으로 Grafana Cloud 서버 메트릭을 Slack에 자동 보고하기
NestJS 스케줄러와 Grafana Cloud Prometheus API를 연결해 매일 아침 서버 상태를 Slack으로 자동 보고하는 시스템을 구축한 과정을 정리했습니다. 이미 갖춰진 인프라를 활용해 최소한의 코드로 일일 리포트를 만드는 방법입니다.
작은 서비스에도 모니터링이 필요한 이유 - NestJS + Prometheus + Grafana Cloud 구축기
작은 서비스도 리소스 제약 때문에 모니터링이 필수입니다. NestJS에 Prometheus와 Grafana Cloud를 연동하여 효율적인 리소스 관리 시스템을 구축하는 방법을 알아보세요.
Claude Code에 Google Search Console MCP를 추가하고, OAuth 토큰 만료를 해결한 경험
GA4 MCP를 운영하다 OAuth 토큰이 만료되고, GSC MCP를 추가하면서 gcloud CLI 설치 오류까지 겪었습니다. ADC 토큰 갱신부터 Python 호환성 문제까지, 실전에서 마주친 트러블슈팅 과정을 정리했습니다.