Beenslab Blog

블로그 방문자 수, 직접 카운팅 시스템 구축기

beenchangseo·2025년 7월 2일Hits

📊 블로그 방문자 수, 직접 카운팅 시스템 구축기

기존에 https://hits.seeyoufarm.com 서비스를 활용해 블로그 방문자 수를 추적하고 있었습니다. 하지만 어느 시점부터 이 서비스가 더 이상 작동하지 않게 되었고, 공식적으로도 유지보수가 중단된 상태였습니다. 사용자 수가 많아지면서 카운트가 정확하게 반영되지 않는 현상도 자주 발생했죠.

그래서 직접 방문자 수 카운팅 시스템을 구축하기로 결심했습니다.
내가 원하는 형식으로 SVG 뱃지를 만들고, 다양한 플랫폼에서도 사용할 수 있도록 API 형태로 제공하는 것이 이번 작업의 핵심 목표였습니다.


🎯 구축 목표 및 기능 요약

  • HTML <img> 태그로 삽입 가능한 SVG 뱃지 생성
  • 방문 시마다 조회수를 Firestore에 저장
  • 다양한 블로그 플랫폼(Velog, Tistory, 개인 블로그)에서도 사용 가능하도록 공개 API 제공
  • 쿼리 스트링의 postId 값을 기준으로 조회수를 구분하여, 각 포스트/페이지별로 개별 카운팅 가능
  • API 응답은 실시간 SVG 이미지로 구성되며 GitHub README나 웹 페이지 내 어디에서든 렌더링 가능
  • 최종적으로는 https://hits.beenslab.com 도메인으로 API를 제공

⚙️ 전체 아키텍처 및 기술 스택

1. AWS Lambda

조회수를 카운트하고, SVG 이미지를 반환하는 핵심 처리 로직을 담당합니다.
TypeScript로 작성되어 있으며, Firebase Admin SDK를 통해 Firestore와 통신합니다.

Lambda 함수 주요 흐름

// src/index.ts (발췌)
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
  const postId = event.queryStringParameters?.postId;
  if (!postId) {
    return {
      statusCode: 400,
      body: 'Missing postId',
    };
  }

  // Firestore에서 조회수 업데이트
  const { totalHits, todayHits } = await updateHitCount(postId);

  // SVG 생성
  const svg = generateSvgBadge(totalHits, todayHits);

  return {
    statusCode: 200,
    headers: { 'Content-Type': 'image/svg+xml' },
    body: svg,
  };
};
  • postId 파라미터를 받아 Firestore에서 해당 포스트의 조회수를 읽고, 증가시킵니다.
  • SVG 뱃지를 동적으로 생성하여 반환합니다.

2. Firebase Firestore

Firestore는 조회수 저장용 NoSQL DB로 선택했습니다.
다음과 같은 구조로 문서를 구성했습니다:

beenslab (문서)
└── posts (컬렉션)
    └── {postId} (문서)
        ├── total_hits: number
        ├── today_hits: number
        └── last_hits_date: string (YYYY-MM-DD)

Firestore 트랜잭션 예시

// src/firestore.ts (발췌)
export async function updateHitCount(postId: string) {
  const postRef = db.collection('posts').doc(postId);
  await db.runTransaction(async (transaction) => {
    const doc = await transaction.get(postRef);
    // ...조회수 증가 및 todayHits, last_hits_date 갱신 로직...
    transaction.set(postRef, { total_hits, today_hits, last_hits_date }, { merge: true });
  });
  // ...생략...
}

3. AWS API Gateway

API Gateway는 퍼블릭한 HTTP 엔드포인트 역할을 하며, Lambda 함수에 요청을 프록시합니다.
GET /hits?postId=... 형태로 호출되며, 응답은 Content-Type: image/svg+xml 헤더를 포함한 SVG 문자열입니다.


4. 커스텀 도메인 연결

  • **AWS Certificate Manager(ACM)**에서 SSL 인증서 발급
  • 외부 도메인 서비스에서 CNAME 레코드로 API Gateway에 연결
  • API Gateway에서 커스텀 도메인 매핑

🌐 다양한 플랫폼에서 사용

HTML 예시

<img src="https://hits.beenslab.com/hits?postId=my-post-uuid" alt="hit-count" />

Velog/Markdown 예시

![Hits](https://hits.beenslab.com/hits?postId=velog-123)
  • postId는 블로그 포스트의 고유 ID 또는 페이지별 구분 키로 자유롭게 설정할 수 있습니다.

🧊 Lambda 콜드 스타트 문제와 CloudWatch 해결책

AWS Lambda의 특성상, 일정 시간 동안 호출이 없으면 함수가 언로드되고, 다음 호출 시 콜드 스타트가 발생합니다.
SVG 이미지 요청처럼 빠른 응답이 필요한 경우 체감될 수 있습니다.

해결 방법

  • CloudWatch EventBridge 규칙을 생성하여 5분마다 Lambda를 호출하도록 설정
  • Lambda를 **웜 상태(warm start)**로 유지하여, 실제 API 응답 속도를 100~300ms 이내로 유지

💻 소스코드

모든 코드는 GitHub에 오픈소스로 공개되어 있습니다:

🔗 beenchangseo/beenslab-hitmark

주요 파일 구조

src/
  ├── index.ts         # Lambda 엔트리포인트
  ├── firestore.ts     # Firestore 트랜잭션 로직
  └── svg.ts           # SVG 뱃지 생성 함수

SVG 생성 함수 예시

// src/svg.ts (발췌)
export function generateSvgBadge(totalHits: number, todayHits: number): string {
  return `
    <svg width="120" height="20" ...>
      <rect ... />
      <text x="10" y="15">Total: ${totalHits}</text>
      <text x="10" y="35">Today: ${todayHits}</text>
    </svg>
  `;
}

📈 향후 개선 방향

  • 중복 카운팅 방지
    • 동일 IP 또는 User-Agent 기반 중복 필터링
    • 일정 시간 이내 중복 요청 제한 등 로직 추가 예정
  • AWS 비용 최적화
    • CloudFront 캐싱, SVG 정적 생성, Lambda 호출량 최적화 등
  • 통계 페이지 제공
    • 관리자/운영자용 대시보드 연동 예정

마치며

직접 방문자 수 카운팅 시스템을 구축하면서

  • 서버리스 환경에서의 실시간 데이터 처리
  • SVG 동적 생성
  • AWS 인프라와 외부 도메인 연동
    등 다양한 경험을 할 수 있었습니다.

누구나 쉽게 자신의 블로그/플랫폼에 방문자 카운터를 붙이고 싶다면
beenslab-hitmark 깃허브에서 소스코드를 참고해보세요!


추가로 궁금한 점이나, 코드 상세 설명이 필요하면 댓글로 남겨주세요!