my-blog 방문자 로깅 시스템 직접 만들어보기 (연재 1/6)
블로그를 운영하면서 누가 어떤 페이지를 보고 가는지 전혀 알 수 없다는 게 계속 신경 쓰였습니다. Astro와 Node 백엔드로 만든 개인 블로그에 방문자 로깅 기능을 처음부터 설계하고 붙여본 과정을 정리합니다.
학습 주제
- 주제: Astro 미들웨어를 활용한 방문자 로깅 시스템 구축
- 날짜: 2026-07-24
탐구 과정
처음 목표는 단순했습니다. "누가 내 블로그에 들어오는지 알고 싶다." 근데 이걸 구현하려니 생각보다 손댈 곳이 많았습니다.
가장 먼저 막힌 지점은 Astro에서 방문자의 실제 IP를 어떻게 잡아내느냐였습니다. Astro 5.18부터 node 어댑터를 쓰면 미들웨어에서 context.clientAddress로 접근할 수 있다는 걸 확인하고 시작했는데, 막상 붙여보니 특정 페이지에서 계속 예외가 터졌습니다.
원인을 추적해보니 clientAddress는 getter로 구현되어 있어서, 접근하는 순간 값이 없으면 즉시 예외를 던지는 구조였습니다. 저는 처음에 try/catch를 함수 안쪽에 넣었는데, 사실 이 getter는 함수 호출 인자를 평가하는 시점, 즉 함수에 진입하기도 전에 이미 평가되어 버렸던 겁니다. try/catch로 감싼 코드 블록에 들어가기도 전에 에러가 나버리니 당연히 잡힐 리가 없었죠. 이 부분을 이해하고 나서야 try/catch 위치를 제대로 조정할 수 있었습니다.
더 큰 문제는 그 다음에 나왔습니다. 빌드를 해봤더니 방문 기록 테이블에 가짜 데이터가 쌓이고 있었던 겁니다. 원인을 찾아보니 /about, /blog, /projects 페이지가 빌드 시점에 정적으로 프리렌더링되고 있었습니다. 즉 이 페이지들은 런타임에 미들웨어를 아예 거치지 않고 정적 파일로 그냥 서빙되는 구조였던 겁니다. 이대로 두면 실제 방문자가 이 세 페이지를 봐도 로그가 하나도 안 남는다는 뜻이었고, 반대로 빌드할 때는 프리렌더링 과정에서 미들웨어가 실행되면서 가짜 방문 기록이 만들어지고 있었던 것이었습니다.
핵심 학습 내용
1. DB 스키마 설계
방문 기록을 위한 테이블을 새로 만들었습니다.
-- blog.visits
-- path, method, ip, user_agent, referrer, created_at
각 컬럼이 어떤 정보를 담는지가 명확해서 나중에 통계 쿼리를 짤 때도 헷갈리지 않았습니다.
2. API 설계 — 내부용과 관리자용 분리
POST /api/visits: 프론트엔드 미들웨어가 방문을 기록할 때 호출하는 내부용 엔드포인트GET /api/visits,GET /api/visits/stats: 관리자만 볼 수 있도록requireAuth로 감싼 통계/목록 조회용 엔드포인트
방문 기록을 남기는 쪽과 조회하는 쪽의 권한 레벨이 완전히 다르다는 걸 명확히 나눈 게 나중에 보안 측면에서도 깔끔했습니다.
3. 미들웨어에서 헤더 캡처하기
nginx 뒤에서 돌아가는 구조라, 실제 클라이언트 IP는 X-Forwarded-For / X-Real-IP 헤더로 넘어옵니다. 미들웨어에서 이 헤더들과 User-Agent, Referer를 잡아서 백엔드로 비동기 전송하도록 만들었습니다.
포인트는 응답 지연 없이 보내는 것과, 정적 자산(파비콘 등) 요청은 로깅에서 제외하는 것이었습니다.
4. 프리렌더 문제 해결
prerender = false
about.astro, blog.astro, projects.astro에 이 설정을 적용해서 SSR로 전환했습니다. 동시에 context.isPrerendered를 체크해서, 혹시 남아있을 프리렌더 경로에서는 로깅 자체를 건너뛰도록 안전장치를 추가했습니다.
5. 대시보드
/admin/visits에서 확인할 수 있는 것들:
- 전체 / 최근 24시간 / 최근 7일 순방문(IP 기준) 통계
- 인기 경로 랭킹
- 최근 방문 목록
로그인이 필요한 페이지로 만들어서 아무나 볼 수 없게 했습니다.
이해한 내용
이번에 가장 크게 배운 건 **"SSR과 프리렌더는 완전히 다른 런타임 경로를 탄다"**는 사실이었습니다. 같은 프로젝트 안에서도 페이지마다 렌더링 방식이 다를 수 있고, 그게 곧 미들웨어를 거치느냐 안 거치느냐를 결정한다는 걸 직접 부딪히면서 체감했습니다. 겉으로 보기엔 다 똑같은 .astro 페이지인데, 내부적으로는 완전히 다르게 동작한다는 게 신기하면서도 무서웠습니다.
또 하나는 getter의 평가 시점 문제입니다. try/catch를 어디에 두느냐가 단순히 "감싸기만 하면 되는" 문제가 아니라, 예외가 실제로 언제 발생하는지 정확히 알아야 제대로 잡을 수 있다는 걸 다시 확인했습니다.
실전 적용
이 로깅 시스템 덕분에 앞으로는:
- 어떤 글이 실제로 많이 읽히는지 데이터 기반으로 확인 가능
- 이상 트래픽(봇, 크롤러 등)을 User-Agent 기반으로 걸러낼 수 있는 기반 마련
- 나중에 지역별/시간대별 방문 패턴 분석으로 확장 가능
작업 마무리 단계에서는 실제 운영 서버에 pm2로 백엔드/프론트엔드를 재시작하고, DB 마이그레이션을 적용하고, nginx 헤더를 시뮬레이션해서 X-Forwarded-For가 제대로 잡히는지까지 end-to-end로 검증했습니다. 테스트 과정에서 쌓인 더미 데이터는 정리해서 지금은 visits 테이블이 깨끗한 상태입니다.
추가 학습 계획
- 방문 통계에 지역(GeoIP) 정보 추가해보기
- 봇/크롤러 트래픽 필터링 로직 고도화
- 방문자 데이터를 시각화 차트로 보여주는 대시보드 개선
- Astro의 SSR/프리렌더 전략을 더 깊게 파고들어서, 어떤 페이지가 어느 쪽이 적합한지 기준 정리해보기