1. 시작하며: 자체 웹 분석 인프라 도입 배경
SvelteKit Blog Engine(SKBE)은 Cloudflare Pages와 D1 기반으로 구동되는 서버리스 블로그 엔진입니다. 이번 SKBE v1.0.1.3 업데이트에서는 외부 써드파티 분석 툴에 의존하지 않고, Cloudflare 인프라 내부에서 블로그 방문자 통계를 자립적으로 수집·저장·시각화할 수 있는 Cloudflare Web Analytics(CFA) 통합 대시보드 및 분석 데이터 다운로드 체계를 구축했습니다.
특히 웹에 공개되는 어드민 페이지의 보안을 위해 토큰을 어드민에 두지 않는 폐쇄형 독립 동기화 Worker(cfa-sync) 아키텍처를 수립하고, 집계 시차로 인한 수치 역행을 방어하는 단조 증가 가드, 그리고 축적된 데이터를 로컬로 추출할 수 있는 CSV/XLSX 다운로드 기능을 완성했습니다.
2. Cloudflare Web Analytics 종합 대시보드 구조
1) 28일 트래픽 및 4분할 환경 분석 UI
어드민 대시보드(apps/admin/src/routes/dashboard) 상단에 Cloudflare Web Analytics 전용 뷰가 렌더링됩니다:
- 최근 28일 트래픽 차트: Chart.js 기반으로 페이지뷰(
Page Views)와 순 방문 세션(Visits)을 일별 라인 그래프로 시각화합니다. - 인기 페이지 Top 5 및 유입 경로 Top 5: 가장 많이 읽힌 포스트 경로(
requestPath)와 외부 유입 도메인(refererHost)을 집계하여 순위별로 표시합니다. 직접 유입은Direct / None으로 결손 없이 매핑합니다. - 4분할 환경 지표 그리드: 접속 국가(
countries), 접속 기기군(devices: Desktop, Mobile, Tablet), 브라우저(browsers), 운영체제(os)의 비율을 4개 카드로 요약 출력합니다.
2) 날짜별 세부 분석 필터 및 다국어 지원
대시보드 상단 툴바를 통해 집계 기간을 즉시 전환할 수 있습니다:
오늘/어제/최근 7일퀵 토글: 단일 클릭으로 일별 스냅샷 데이터를 즉시 교체합니다.특정 날짜 선택 (YYYY-MM-DD)드롭다운: D1에 축적된 과거 특정 일자의 디멘션 통계를 단독 조회할 수 있습니다.- 모든 통계 UI 텍스트는 한국어, 영어, 일본어 3개 국어 딕셔너리(
t())에 정식 등록되어 관리자의 언어 설정에 맞춰 표시됩니다.
3. 보안 격리 아키텍처: 공격 표면 제로(Zero-Attack-Surface) 폐쇄형 Worker
CFA 데이터를 어드민에서 표시하기 위해 가장 직관적인 방법은 어드민 서버(+page.server.ts)가 Cloudflare GraphQL API를 직접 호출하는 방식입니다. 그러나 이 방식은 어드민 환경변수에 계정의 CF_API_TOKEN을 상시 보관해야 하므로 보안상 매우 위험합니다.
SKBE는 **"어드민은 권한을 갖지 않고, D1의 정적 스냅샷만 읽는다"**는 단방향 보안 격리 아키텍처를 수립했습니다:
┌────────────────────────────────────────────────────────────────────────┐
│ Cloudflare Private Network (외부 비노출 폐쇄망) │
│ │
│ [Cloudflare Analytics] │
│ │ (GraphQL RUM API) │
│ ▼ │
│ [cfa-sync Worker] (workers_dev: false, 크론 스케줄러: 0 */1 * * *) │
│ │ (D1 Binding: INSERT / UPDATE) │
│ ▼ │
│ [D1 Database] ── (cf_daily_snapshots, visitor_stats) │
└─────────────┬──────────────────────────────────────────────────────────┘
│ (Read Only SELECT)
▼
┌────────────────────────────────────────────────────────────────────────┐
│ [Admin Pages] (skbeadmin.pages.dev) │
│ * CFA API 토큰 미보유 (보안 격리 달성) │
│ * D1에 적재된 스냅샷만 안전하게 읽어와 렌더링 │
└────────────────────────────────────────────────────────────────────────┘
- 외부 주소 비활성화 (
workers_dev: false):cfa-syncWorker는 외부 인터넷에서 접속할 수 있는 도메인이나 라우트를 일절 갖지 않습니다. 외부에서 HTTP 요청을 보낼 수 있는 엔드포인트 자체가 없으므로 외부 공격 표면(Attack Surface)은 물리적으로 0입니다. - Cloudflare 내부 크론 구동:
오직 Cloudflare 시스템 내부 스케줄러(triggers: { crons: ['0 */1 * * *'] })에 의해서만 매시간 정각 백그라운드로 실행됩니다. - D1 스냅샷 적재:
Worker는 GraphQL API에서 데이터를 수집하여 D1의cf_daily_snapshots와visitor_stats에 저장하고 즉시 종료됩니다.
4. 데이터 정합성 보장: 분산 집계 시차 방어 및 단조 증가 가드
Cloudflare Web Analytics의 rumPageloadEventsAdaptiveGroups는 전 세계 글로벌 엣지 노드에서 집계되는 분산 데이터셋입니다. 엣지 노드별 로그 집계 시차나 일시적 수집 지연이 발생할 수 있는 특성을 고려하여, 동기화 엔진(cfa-sync)에 데이터 정합성을 위한 보호 메커니즘을 적용했습니다.
1) 오늘과 과거 데이터(최근 7일) 처리 기준의 분리
- 과거 데이터 (최근 7일 자동 보정):
글로벌 엣지 지연 집계 보정을 반영하기 위해 매시간 실행 시 최근 7일치를 함께 조회하며, 해시(data_hash) 비교를 통해 기존 값과 달라진 변경점이 감지되면 D1 데이터를 자동으로 최신 수치로 덮어씌웁니다. - 오늘 데이터 (실시간 누적):
실시간으로 트래픽이 계속 누적되는 진행형 데이터이므로, 수집 시차로 인해 신규 수치가 기존 기록보다 작게 들어오는 역행 현상을 방지하는 단조 증가 가드를 적용합니다.
2) 오늘 데이터 역행(조회수 축소) 방지 가드 구현
// apps/cfa-sync/src/index.ts
for (const snapshot of snapshots) {
const isToday = snapshot.date === todayStr;
const prevViews = existingViews.get(snapshot.date);
// 오늘(Today) 데이터 역행(조회수 축소) 방지 가드:
// 외부 수집 지연 등으로 신규 수치가 기존 D1 수치보다 작게 들어온 경우 덮어쓰지 않고 기존 데이터 보존
if (isToday && prevViews !== undefined && snapshot.pageViews < prevViews) {
console.warn(`[CFA] Regression detected for today (${snapshot.date}): new(${snapshot.pageViews}) < existing(${prevViews}). Skipping update to protect data.`);
continue;
}
// 데이터 해시가 변경된 경우에만 D1 쓰기 수행
const hash = await sha256(JSON.stringify({ ... }));
if (existingHashes.get(snapshot.date) === hash) continue;
statements.push(env.BLOG_DB.prepare(`INSERT INTO cf_daily_snapshots ...`));
statements.push(env.BLOG_DB.prepare(`INSERT INTO visitor_stats ...`));
written++;
}
이 가드를 통해 분산 환경의 집계 시차나 일시적인 지연 상황에서도 이미 확보된 오늘 누적 트래픽 통계가 축소되거나 오염되지 않도록 D1 데이터의 무결성을 안전하게 보존합니다.
5. Cloudflare Web Analytics 분석 데이터 다운로드(CSV / XLSX) 기능 구현
1) 분석 데이터 다운로드 기능 도입 배경
대시보드 화면상에서 일별 트래픽과 디멘션을 실시간 확인하는 것에 그치지 않고, 장기 데이터 백업, 외부 보고용 스프레드시트 결산, 통계 2차 가공을 위해 D1 데이터베이스에 축적된 일자별 스냅샷(cf_daily_snapshots)을 사용자가 로컬 파일로 직접 추출할 수 있는 전용 내보내기 다운로드 기능이 신설되었습니다.
2) 다운로드 인터페이스 및 옵션 설계
어드민 대시보드 상단의 분석 프로바이더 토글 영역에 CSV/XLSX 내보내기 트리거 버튼이 추가되었으며, 클릭 시 설정 모달 팝업이 활성화됩니다:
- 기간 선택:
최근 7일,최근 28일,이번 달,최근 90일 (분기),1년 결산 (연간) - 리포트 형태 선택:
- 종합 분석 리포트 (CSV): 트래픽 추이 및 5대 세부 디멘션을 단일 보고서로 다운로드
- 맞춤 항목 선택 (XLSX): 사용자가 원하는 항목만 선택하여 개별 탭으로 분리 다운로드
- 추출 세부 항목 체크박스:
일별 트래픽 추이,인기 페이지,유입 출처,접속 국가,브라우저,장치 유형
3) 2가지 포맷별 아키텍처 및 구현 방식
A. 종합 분석 리포트: 1일 1행 압축 와이드 CSV 규격
통상적인 통계 추출 시 중간에 표 헤더나 텍스트가 섞이면 Excel 자동 필터나 피벗 테이블 생성이 깨지는 문제가 발생합니다. 이를 방지하기 위해 완전한 정규 단일 테이블 형태를 채택했습니다:
- 압축 와이드 구조:
일자, 순 방문자, 페이지뷰, 주요 유입 출처 (Top 3), 주요 방문 페이지 (Top 3), 접속 국가 (Top 3), 주 브라우저 (Top 2), 주 장치 (Top 2) - 연간 결산 고속 처리: 365일 기간을 선택하더라도 D1 스냅샷을 1일 1행으로 압축 결합하여 정확히 365행의 깔끔한 단일 표로 생성됩니다.
- 엑셀 한글 호환성: UTF-8 BOM(
\uFEFF) 바이트를 선두에 주입하여 Microsoft Excel에서 열었을 때 한글이나 다국어가 깨지지 않도록 인코딩을 보장합니다.
B. 맞춤 항목 선택: SheetJS 연동 멀티시트 XLSX 추출
단일 플랫 CSV에서 서로 다른 성격의 디멘션(예: 페이지 URL, 국가명, 브라우저명)을 한 열에 몰아넣으면 데이터 정렬과 집계가 모호해집니다. 이 한계를 극복하기 위해 SheetJS (xlsx) 엔진을 도입했습니다:
- 독립 시트 분리: 사용자가 선택한 섹션만 각각
트래픽 추이,인기 페이지,유입 출처,접속 국가,브라우저,장치 유형탭으로 분리 생성됩니다. - 시트별 전용 컬럼 최적화: 인기 페이지 시트는 페이지뷰(
Page Views) 중심, 유입 출처/국가/브라우저/장치 시트는 방문자수(Visitors) 중심으로 컬럼 헤더가 시트별 성격에 맞게 자동 구성됩니다. - 엣지 런타임 호환성: Cloudflare Workers 환경에서 Node.js 네이티브 바인딩 없이 순수 메모리 버퍼(
type: 'buffer', bookType: 'xlsx')로 이진 파일을 고속 직렬화합니다.
4) 다국어 및 다운로드 파이프라인 안전장치
- 중앙 집중식 i18n 연동: 컬럼 헤더와 시트명은 로컬 하드코딩을 배제하고
@blog/shared/i18n의fallbackDictionary를 직접 참조하여 관리자의 사용 언어(한국어, 영어, 일본어)에 맞춰 자동 번역됩니다. - 동적 파일명 및 MIME 타입 할당: 서버 응답의
Content-Disposition헤더와 브라우저 가상 앵커 태그(a.download)를 정합시켜, 내보내기 방식에 따라skbe_analytics_28d_2026-10-31.csv또는.xlsx확장자가 정확하게 부여됩니다.
6. 실무 가이드: 멀티 계정 배포 커맨드 및 파이프라인 완전 해설
SKBE는 단일 개발 환경에서 여러 개의 독립된 블로그 계정(예: 메인 블로그, 서브 블로그 등)을 손쉽게 운영할 수 있도록 멀티 계정 배포 파이프라인을 지원합니다.
1) 계정 설정 구성 (.deploy-accounts.json)
프로젝트 루트의 .deploy-accounts.json 파일에 각 계정별 D1, Pages 프로젝트명, 그리고 CFA 설정(cfa)을 정의합니다:
{
"main": {
"name": "메인 블로그",
"token": "여기에_Cloudflare_API_토큰 (또는 메인 웹 로그인 계정인 경우 플레이스홀더)",
"accountId": "클라우드플레어_계정_ID",
"blogProjectName": "skbeblog",
"adminProjectName": "skbeadmin",
"blogDb": {
"name": "skbeblog-db",
"id": "D1_데이터베이스_UUID"
},
"cfa": {
"cfApiToken": "CFA_조회_권한이_있는_Cloudflare_API_토큰",
"cfSiteTag": "Web_Analytics_사이트_태그",
"workerName": "skbe-cfa-sync"
}
},
"second": {
"name": "서브 블로그",
"token": "서브_계정_전용_Cloudflare_API_토큰",
"accountId": "서브_계정_ID",
"blogProjectName": "second-blog",
"adminProjectName": "second-admin",
"blogDb": {
"name": "second-blog-db",
"id": "서브_D1_UUID"
},
"cfa": {
"cfApiToken": "서브_CFA_API_토큰",
"cfSiteTag": "서브_사이트_태그",
"workerName": "second-cfa-sync"
}
}
}
2) 배포 커맨드 레퍼런스
A. 멀티 계정 통합 배포 (deploy:multi)
지정한 계정의 블로그, 어드민, 그리고 CFA Worker를 순차적으로 빌드 및 배포합니다.
# 특정 계정의 전체(블로그 + 어드민 + CFA Worker) 배포
npm run deploy:multi <계정키>
# 예시: npm run deploy:multi main
# 예시: npm run deploy:multi second
# 특정 계정의 특정 앱만 배포 (blog 또는 admin)
npm run deploy:multi <계정키> blog
npm run deploy:multi <계정키> admin
# 설정된 모든 계정 일괄 순차 배포
npm run deploy:multi --all
- 동작 순서:
- 대상 계정 D1 스키마 자동 마이그레이션(
schema-blog-db.sql,schema-user-db.sql) - SvelteKit 앱 빌드 및 Cloudflare Pages 배포
cfa섹션이 정의되어 있는 경우,cfa-syncWorker 자동 배포 및 Secrets 주입
- 대상 계정 D1 스키마 자동 마이그레이션(
B. CFA Sync Worker 단독 배포 (deploy:cfa-sync)
블로그나 어드민의 재빌드 없이, CFA Worker 코드만 단독으로 배포할 때 사용하는 명령어입니다.
# 사용법 확인 및 등록된 계정별 CFA 설정 상태 출력
npm run deploy:cfa-sync
# 특정 계정의 CFA Worker 단독 배포
npm run deploy:cfa-sync -- <계정키>
# 예시: npm run deploy:cfa-sync -- main
# 예시: npm run deploy:cfa-sync -- second
# 등록된 모든 계정의 CFA Worker 일괄 배포
npm run deploy:cfa-sync -- --all
C. 단일 기본 배포 (deploy:admin, deploy:blog)
단일 계정 환경에서 사용하는 표준 커맨드입니다. deploy:admin 실행 시 메인 계정의 CFA Worker 배포 단계가 자동으로 연동되어 있습니다.
# 메인 어드민 배포 + 메인 CFA Worker 자동 연동
npm run deploy:admin
# 메인 블로그 배포
npm run deploy:blog
3) 중복 배포 방지 및 강제 갱신 (--force 플래그)
deploy-cfa-sync.js는 불필요한 Cloudflare API 호출과 중복 배포 시간을 줄이기 위해 워커 존재 사전 검사를 수행합니다:
⏩ [main] [KO] CFA Sync Worker ("skbe-cfa-sync")가 이미 등록되어 정상 작동 중입니다. 배포를 건너뜁니다.
[EN] CFA Sync Worker ("skbe-cfa-sync") is already active. Skipping deployment.
[KO] 강제 재배포 필요 시: --force / [EN] For forced redeployment: --force
- 건너뛰기 조건: 대상 Cloudflare 계정에 해당 이름의 Worker가 이미 활성화되어 있으면 배포를 스킵합니다.
- 강제 재배포 (
--force):
Worker 소스코드(apps/cfa-sync/src/index.ts)를 수정하여 Cloudflare에 최신 코드를 즉시 반영해야 할 때는 명령어 뒤에--force플래그를 붙여 실행합니다:# Worker 최신 코드 강제 반영 배포 npm run deploy:cfa-sync -- <계정키> --force # 예시: npm run deploy:cfa-sync -- main --force
7. 마치며
이번 SKBE v1.0.1.3 업데이트를 통해, 외부 추적 스크립트나 무거운 써드파티 의존성 없이도 Cloudflare 자체 인프라만으로 안정적이고 정밀한 방문자 분석 환경을 갖추게 되었습니다.
특히 어드민의 API 토큰 보유를 금지한 폐쇄형 격리 Worker 모델과, 분산 엣지 집계 특성을 반영한 단조 증가 역행 방지 가드는 서버리스 환경에서 데이터의 무결성과 보안을 동시에 확보할 수 있는 견고한 토대가 되었습니다.
향후에도 엣지 환경에 특화된 경량성과 데이터 보호 원칙을 기반으로 블로그 코어 엔진을 지속적으로 고도화해 나갈 예정입니다.
댓글 0개
댓글을 작성하려면 로그인이 필요합니다.