전체 워크플로우 구조
Schedule Trigger → GA4 Data Extract → Data Processing → Report Generation → Email Send
1. 사전 준비 작업
Google Analytics API 설정
- Google Cloud Console 에서 프로젝트 생성
- Google Analytics Reporting API v4 활성화
- 서비스 계정 생성 및 JSON 키 파일 다운로드
- 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. 배포 및 모니터링
워크플로우 테스트
- Manual Trigger로 워크플로우 테스트
- Error Handling 노드 추가
- 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 테스트 분석: 실험 결과 자동 분석