n8n을 이용한 GA4 자동 리포팅 워크플로우

전체 워크플로우 구조

Schedule Trigger → GA4 Data Extract → Data Processing → Report Generation → Email Send

1. 사전 준비 작업

Google Analytics API 설정

  1. Google Cloud Console 에서 프로젝트 생성
  2. Google Analytics Reporting API v4 활성화
  3. 서비스 계정 생성 및 JSON 키 파일 다운로드
  4. GA4 속성에 서비스 계정 이메일을 뷰어 권한으로 추가

n8n 환경 설정

  • Google Analytics 4 노드 사용을 위한 인증 정보 설정
  • 이메일 발송을 위한 SMTP 설정 또는 이메일 서비스 연동

2. n8n 워크플로우 구성

2.1 Schedule Trigger (스케줄 트리거)

json{
  "rule": {
    "interval": [{
      "field": "weekday",
      "value": 1
    }, {
      "field": "hour", 
      "value": 9
    }]
  }
}
  • 설정: 매주 월요일 오전 9시 실행
  • 용도: 주간 리포트 자동 생성 트리거

2.2 GA4 Data Extract (데이터 추출)

노드: Google Analytics 4 노드 또는 HTTP Request 노드

javascript// GA4 Reporting API 요청 예시
{
  "property": "properties/YOUR_PROPERTY_ID",
  "requests": [{
    "dimensions": [
      {"name": "date"},
      {"name": "pagePath"},
      {"name": "country"},
      {"name": "deviceCategory"}
    ],
    "metrics": [
      {"name": "sessions"},
      {"name": "users"},
      {"name": "pageviews"},
      {"name": "bounceRate"},
      {"name": "sessionDuration"}
    ],
    "dateRanges": [
      {
        "startDate": "7daysAgo",
        "endDate": "yesterday"
      }
    ],
    "orderBys": [
      {
        "metric": {"metricName": "sessions"},
        "desc": true
      }
    ],
    "limit": 100
  }]
}

2.3 Data Processing (데이터 처리)

노드: Code 노드 (JavaScript)

javascript// 데이터 분석 및 가공
const rawData = items[0].json;
const report = rawData.reports[0];
const rows = report.data.rows || [];

// 주요 지표 계산
const totalSessions = rows.reduce((sum, row) => sum + parseInt(row.metrics[0].values[0]), 0);
const totalUsers = rows.reduce((sum, row) => sum + parseInt(row.metrics[0].values[1]), 0);
const totalPageviews = rows.reduce((sum, row) => sum + parseInt(row.metrics[0].values[2]), 0);

// 상위 페이지 분석
const topPages = rows
  .sort((a, b) => parseInt(b.metrics[0].values[0]) - parseInt(a.metrics[0].values[0]))
  .slice(0, 10)
  .map(row => ({
    page: row.dimensions[1],
    sessions: parseInt(row.metrics[0].values[0]),
    users: parseInt(row.metrics[0].values[1])
  }));

// 디바이스별 분석
const deviceData = {};
rows.forEach(row => {
  const device = row.dimensions[3];
  if (!deviceData[device]) {
    deviceData[device] = { sessions: 0, users: 0 };
  }
  deviceData[device].sessions += parseInt(row.metrics[0].values[0]);
  deviceData[device].users += parseInt(row.metrics[0].values[1]);
});

// 국가별 분석
const countryData = {};
rows.forEach(row => {
  const country = row.dimensions[2];
  if (!countryData[country]) {
    countryData[country] = { sessions: 0, users: 0 };
  }
  countryData[country].sessions += parseInt(row.metrics[0].values[0]);
  countryData[country].users += parseInt(row.metrics[0].values[1]);
});

const topCountries = Object.entries(countryData)
  .sort(([,a], [,b]) => b.sessions - a.sessions)
  .slice(0, 5)
  .map(([country, data]) => ({ country, ...data }));

return [{
  json: {
    summary: {
      totalSessions,
      totalUsers,
      totalPageviews,
      period: "지난 7일"
    },
    topPages,
    deviceBreakdown: Object.entries(deviceData).map(([device, data]) => ({ device, ...data })),
    topCountries,
    generatedAt: new Date().toISOString()
  }
}];

2.4 Report Generation (리포트 생성)

노드: Code 노드 또는 HTML 노드

javascript// HTML 리포트 생성
const data = items[0].json;
const { summary, topPages, deviceBreakdown, topCountries } = data;

const htmlReport = `
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>주간 웹사이트 분석 리포트</title>
    <style>
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; }
        .container { max-width: 800px; margin: 0 auto; background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; border-radius: 8px 8px 0 0; }
        .content { padding: 30px; }
        .metric-card { background: #f8f9fa; border-left: 4px solid #007bff; padding: 20px; margin: 15px 0; border-radius: 4px; }
        .metric-value { font-size: 2em; font-weight: bold; color: #007bff; }
        .metric-label { color: #6c757d; margin-top: 5px; }
        .section { margin: 30px 0; }
        .section h3 { color: #343a40; border-bottom: 2px solid #dee2e6; padding-bottom: 10px; }
        table { width: 100%; border-collapse: collapse; margin: 15px 0; }
        th, td { padding: 12px; text-align: left; border-bottom: 1px solid #dee2e6; }
        th { background-color: #f8f9fa; font-weight: 600; }
        .top-item { background-color: #fff3cd; }
        .footer { text-align: center; color: #6c757d; padding: 20px; font-size: 0.9em; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>📊 주간 웹사이트 분석 리포트</h1>
            <p>${summary.period} 데이터</p>
        </div>
        
        <div class="content">
            <div class="section">
                <h3>🎯 주요 지표 요약</h3>
                <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
                    <div class="metric-card">
                        <div class="metric-value">${summary.totalSessions.toLocaleString()}</div>
                        <div class="metric-label">총 세션수</div>
                    </div>
                    <div class="metric-card">
                        <div class="metric-value">${summary.totalUsers.toLocaleString()}</div>
                        <div class="metric-label">순 사용자수</div>
                    </div>
                    <div class="metric-card">
                        <div class="metric-value">${summary.totalPageviews.toLocaleString()}</div>
                        <div class="metric-label">페이지뷰</div>
                    </div>
                </div>
            </div>

            <div class="section">
                <h3>📄 상위 페이지 (세션 기준)</h3>
                <table>
                    <thead>
                        <tr><th>순위</th><th>페이지</th><th>세션수</th><th>사용자수</th></tr>
                    </thead>
                    <tbody>
                        ${topPages.map((page, index) => 
                            `<tr class="${index === 0 ? 'top-item' : ''}">
                                <td>${index + 1}</td>
                                <td>${page.page}</td>
                                <td>${page.sessions.toLocaleString()}</td>
                                <td>${page.users.toLocaleString()}</td>
                            </tr>`
                        ).join('')}
                    </tbody>
                </table>
            </div>

            <div class="section">
                <h3>📱 디바이스별 분석</h3>
                <table>
                    <thead>
                        <tr><th>디바이스</th><th>세션수</th><th>사용자수</th><th>비율</th></tr>
                    </thead>
                    <tbody>
                        ${deviceBreakdown.map(device => {
                            const percentage = ((device.sessions / summary.totalSessions) * 100).toFixed(1);
                            return `<tr>
                                <td>${device.device}</td>
                                <td>${device.sessions.toLocaleString()}</td>
                                <td>${device.users.toLocaleString()}</td>
                                <td>${percentage}%</td>
                            </tr>`;
                        }).join('')}
                    </tbody>
                </table>
            </div>

            <div class="section">
                <h3>🌍 상위 국가별 분석</h3>
                <table>
                    <thead>
                        <tr><th>순위</th><th>국가</th><th>세션수</th><th>사용자수</th></tr>
                    </thead>
                    <tbody>
                        ${topCountries.map((country, index) => 
                            `<tr class="${index === 0 ? 'top-item' : ''}">
                                <td>${index + 1}</td>
                                <td>${country.country}</td>
                                <td>${country.sessions.toLocaleString()}</td>
                                <td>${country.users.toLocaleString()}</td>
                            </tr>`
                        ).join('')}
                    </tbody>
                </table>
            </div>
        </div>
        
        <div class="footer">
            <p>리포트 생성 시간: ${new Date(data.generatedAt).toLocaleString('ko-KR')}</p>
            <p>본 리포트는 Google Analytics 4 데이터를 기반으로 자동 생성되었습니다.</p>
        </div>
    </div>
</body>
</html>
`;

return [{ json: { htmlReport, summary, reportDate: new Date().toISOString() } }];

2.5 Email Send (이메일 발송)

노드: Send Email 노드

json{
  "to": "client@example.com",
  "subject": "주간 웹사이트 분석 리포트 - {{$now.format('YYYY년 MM월 DD일')}}",
  "html": "{{$json.htmlReport}}",
  "attachments": []
}

3. 고급 기능 추가

3.1 전주 대비 성장률 분석

javascript// 전주 데이터 비교를 위한 추가 API 호출
const thisWeekData = /* 이번 주 데이터 */;
const lastWeekData = /* 지난 주 데이터 */;

const growthRate = {
  sessions: ((thisWeekData.sessions - lastWeekData.sessions) / lastWeekData.sessions * 100).toFixed(1),
  users: ((thisWeekData.users - lastWeekData.users) / lastWeekData.users * 100).toFixed(1),
  pageviews: ((thisWeekData.pageviews - lastWeekData.pageviews) / lastWeekData.pageviews * 100).toFixed(1)
};

3.2 알림 조건 설정

javascript// 특정 조건 시 별도 알림
const alerts = [];

if (summary.totalSessions < 1000) {
  alerts.push("⚠️ 주간 세션수가 평소보다 낮습니다.");
}

if (deviceBreakdown.find(d => d.device === 'mobile')?.sessions / summary.totalSessions < 0.6) {
  alerts.push("📱 모바일 트래픽 비율이 낮습니다.");
}

3.3 다중 고객 관리

javascript// 고객별 설정 관리
const clients = [
  { 
    email: "client1@example.com", 
    propertyId: "properties/123456789",
    customMetrics: ["customEvent:purchase"]
  },
  { 
    email: "client2@example.com", 
    propertyId: "properties/987654321",
    customMetrics: ["customEvent:signup"]
  }
];

4. 배포 및 모니터링

워크플로우 테스트

  1. Manual Trigger로 워크플로우 테스트
  2. Error Handling 노드 추가
  3. Execution Log 확인

에러 처리

javascripttry {
  // GA4 API 호출
} catch (error) {
  // 에러 로깅 및 관리자 알림
  return [{
    json: {
      error: true,
      message: "GA4 데이터 추출 실패",
      details: error.message
    }
  }];
}

성능 최적화

  • API 호출 최소화
  • 데이터 캐싱 고려
  • 배치 처리 적용

5. 확장 가능한 기능

예측 분석: 트렌드 예측 및 인사이트 제공

PDF 리포트 생성: Puppeteer 사용

Slack/Teams 연동: 즉석 알림

대시보드 연동: Grafana, Tableau 등

A/B 테스트 분석: 실험 결과 자동 분석

공유하기

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다