← 글 목록

개인 블로그에 방문자 로깅 시스템 만들기 (Astro + Node) (연재 1/3)

/ 6분 분량

내 블로그에 누가 들어오는지 전혀 알 수 없다는 게 계속 마음에 걸려서, 방문자 로깅 시스템을 직접 설계하고 붙여봤다. 그 과정에서 SSR과 정적 프리렌더링의 차이를 제대로 이해하게 됐다.

학습 주제

  • Astro 미들웨어를 이용한 방문 로그 수집
  • Node/Express 백엔드에 방문 통계 API 추가
  • 학습 날짜: 2026-07-24

블로그(Astro 프론트엔드 + Node 백엔드, Postgres, pm2/nginx로 운영 중)에 "어떤 사람이 들어오는가"를 파악할 수 있는 최소한의 로깅 기능을 넣는 게 목표였다.

탐구 과정

처음엔 단순하게 생각했다. 요청이 들어올 때마다 IP, User-Agent, Referer 정도만 기록하면 되지 않을까. 그런데 실제로 붙여보니 예상치 못한 문제가 두 개나 튀어나왔다.

첫 번째 삽질: clientAddress가 자꾸 죽는다.
Astro의 context.clientAddress는 어댑터에 따라 접근 시점에 예외를 던질 수 있는 getter라는 걸 몰랐다. 나는 당연히 함수 안에서 try/catch로 감싸면 안전할 거라 생각했는데, 실제로는 이 getter가 호출부(call site)에서 먼저 평가되기 때문에 내가 짜놓은 try/catch 블록에 도달하기도 전에 예외가 터졌다. 빌드 로그를 보고서야 "어? try/catch 안에 있는데 왜 죽지?"를 몇 번 되뇌다가, 예외 발생 위치를 다시 추적해보고 나서야 이해했다. try/catch의 위치 자체를 옮겨야 하는 문제였다.

두 번째, 더 근본적인 삽질: 프리렌더 페이지는 미들웨어를 안 탄다.
로그가 잘 쌓이는지 확인하려고 페이지들을 하나씩 눌러봤는데, /about, /blog, /projects만 유독 로그가 하나도 안 남았다. 처음엔 미들웨어 코드가 잘못됐다고 생각해서 한참 들여다봤는데, 알고 보니 이 세 페이지는 빌드 시점에 정적 HTML로 미리 만들어지는(prerender) 페이지였다. 정적 파일은 서버 요청 자체가 미들웨어를 거치지 않고 nginx가 바로 응답해버리니, 애초에 로깅될 방법이 없었던 거다. "빌드는 성공했는데 실제로는 절반이 추적 안 되는" 상황을 뒤늦게 알아차린 셈이다.

핵심 학습 내용

1. 방문 로그 스키마 설계

크게 욕심내지 않고 꼭 필요한 컬럼만 넣었다.

-- blog.visits
path        text
method      text
ip          text
user_agent  text
referrer    text
created_at  timestamptz

2. 백엔드 API 구조

  • POST /api/visits — 내부용, 미들웨어가 방문 발생 시 비동기로 호출
  • GET /api/visits, GET /api/visits/stats — 관리자 전용(requireAuth), 대시보드에서 조회

3. Astro 미들웨어에서 실제 방문자 정보 잡기

nginx 뒤에서 돌아가는 구조라, 진짜 클라이언트 IP는 X-Forwarded-For / X-Real-IP 헤더로 넘어온다.

// middleware.js (개념 정리)
if (!context.isPrerendered) {
  const ip = request.headers.get('x-forwarded-for')
    ?? request.headers.get('x-real-ip')
    ?? context.clientAddress;
  // User-Agent, Referer도 함께 수집
  // 정적 자산 요청은 제외
  // fetch는 await 하지 않고 비동기로 보내서 응답 지연 없게
}

여기서 context.isPrerendered라는 플래그가 핵심이었다. 이걸 체크하지 않으면 빌드할 때마다 가짜 방문 로그가 DB에 쌓이는 부작용까지 생긴다는 걸 실제로 겪어보고 알았다.

4. 프리렌더 → SSR 전환

정적 페이지로는 방문 추적이 안 된다는 걸 확인한 뒤, 해당 페이지들의 prerender 옵션을 false로 바꿔서 다른 페이지들처럼 요청마다 서버가 처리하도록 통일했다.

이해한 내용

  • 정적 프리렌더링 vs SSR의 실질적 차이: 둘 다 "페이지를 보여준다"는 결과는 같아 보이지만, 프리렌더는 요청 자체가 서버 로직(미들웨어 포함)을 아예 거치지 않는다는 걸 몸으로 체감했다. 로깅, 인증 체크처럼 "요청마다 실행돼야 하는 로직"이 있다면 프리렌더는 애초에 후보가 아니라는 걸 알게 됐다.
  • getter 형태의 프로퍼티는 접근 시점 자체가 위험할 수 있다: try/catch는 코드 블록의 위치가 실제 예외 발생 시점과 일치해야 의미가 있다. 표현식 평가 순서를 놓치면 방어 코드를 짜놓고도 무용지물이 될 수 있다는 걸 배웠다.
  • 비동기 로깅은 반드시 "fire-and-forget"으로: 방문 로그를 남기느라 실제 페이지 응답이 늦어지면 본말이 전도된다. 로깅 요청은 응답을 기다리지 않고 던져두는 방식이 맞다는 걸 확인했다.
  • 엔드투엔드 검증의 중요성: nginx 헤더가 실제로 잘 전달되는지, 외부 요청이 막히는 네트워크 환경에서는 로컬에서 헤더를 흉내 내서라도 검증해야 한다는 걸 알았다. 실제로 외부 curl이 응답 없이 걸려서, nginx가 붙이는 헤더를 로컬 요청에 직접 실어 보내는 방식으로 우회 검증했다.

실전 적용

  • /admin/visits 대시보드에서 전체/24시간/7일 순방문(IP 기준) 통계와 인기 경로, 최근 방문 목록을 확인할 수 있게 됐다.
  • 앞으로는 어떤 글이 실제로 많이 읽히는지 데이터 기반으로 파악해서, 다음 글 주제를 정할 때 참고할 계획이다.
  • 봇/크롤러 트래픽과 실제 사람 방문을 구분하는 필터링(User-Agent 기반)도 추가로 고려해볼 만하다.

추가 학습 계획

  • IP만으로는 정확한 순방문자 파악에 한계가 있어서, 쿠키나 세션 기반 방문자 식별 방식도 비교해보고 싶다.
  • Astro의 SSR/prerender 옵션이 페이지별 캐싱 전략과는 어떻게 상호작용하는지 더 깊이 파보기.
  • 봇 트래픽 필터링을 위한 User-Agent 패턴 정리.
  • pm2로 운영 중인 서비스를 무중단으로 재시작하는 방법(graceful reload) 알아보기.