블로그 플랫폼을 만들면서 제일 늦게 손댄 기능이 분석(analytics)이었다. 의도적으로 미뤘다기보다는, 다른 게 더 급했다. 에디터, 발행, 테마, 댓글까지 다 올린 다음에야 "그래서 내 글을 몇 명이나 읽고 있는 거지?"라는 질문이 떠올랐다.
근데 그 질문에 답하려고 Google Analytics 스니펫을 붙이는 순간, Postlark가 지켜온 한 가지 원칙이 깨진다. 클라이언트 JS 0바이트.
왜 JS를 안 쓰는가
Postlark의 블로그 페이지는 서버에서 완성된 HTML과 CSS만 내려보낸다. React도 없고 번들러도 없다. 독자의 브라우저에서 실행되는 자바스크립트라고는 giscus 댓글 위젯과 다크모드 토글 정도가 전부다.
이건 성능 때문만은 아니다. 물론 JS가 없으면 빠르다 — 파싱도 실행도 없으니까. 하지만 진짜 이유는 블로그라는 매체의 본질에 있다. 블로그 글은 텍스트와 이미지다. 그걸 보여주는 데 자바스크립트가 왜 필요한가? 필요 없다. 그래서 안 쓴다.
GA를 붙이면 이 전제가 무너진다. gtag.js 하나가 약 90KB. 그 스크립트가 하는 일은 독자의 브라우저 정보, 스크롤 위치, 체류 시간, 리퍼러를 수집해서 Google 서버로 보내는 것이다. 독자가 원한 건 글을 읽는 것뿐인데, 백그라운드에서 추적 스크립트가 돌아간다. 이게 마음에 걸렸다.
서버에서 세면 된다
발상의 전환은 간단했다. 독자가 글 페이지를 요청하면, 그 요청을 처리하는 서버가 이미 존재한다. 서버가 응답을 만드는 그 시점에 "이 글에 방문이 하나 있었다"고 기록하면 된다. 클라이언트에 아무것도 심을 필요가 없다.
구현 자체는 단순했다. 글 페이지 요청이 들어오면 조회수를 1 올리고, 그 작업은 응답 반환과 비동기로 처리한다. 독자가 HTML을 받는 속도에 영향을 주지 않는다. 기록은 날짜별로 쌓이고, 90일이 지나면 자동으로 사라진다.
이 방식의 장점을 정리하면 이렇다:
독자 브라우저에 추적 코드가 없으므로 광고 차단기에 영향받지 않는다
쿠키를 사용하지 않으므로 GDPR 배너가 필요 없다
페이지 로드 시간에 0ms를 추가한다
서버 로그가 아니라 구조화된 데이터로 쌓이므로 대시보드에서 바로 조회 가능하다
봇을 걸러내는 게 진짜 문제였다
조회수를 세는 건 쉬웠다. 진짜 어려운 건 "사람"만 세는 것이었다.
서버 사이드 분석의 가장 큰 함정이 여기 있다. Googlebot, Bingbot, 각종 SEO 크롤러, 업타임 모니터, RSS 리더 — 이것들이 전부 조회수에 잡힌다. 클라이언트 사이드 분석은 자바스크립트를 실행할 수 있는 환경에서만 동작하기 때문에 봇 대부분이 자연스럽게 걸러진다. 서버 사이드에서는 그 필터가 없다.
처음 분석 기능을 켰을 때 조회수가 비정상적으로 높게 나왔다. "이렇게 많이 읽히나?" 싶어서 기분이 좋았는데, User-Agent를 까보니 절반 이상이 봇이었다. 20종 넘는 봇 패턴을 수집해서 필터를 만들었다. 알려진 크롤러야 User-Agent에 이름이 박혀 있으니 쉽지만, 문제는 UA를 위장하거나 아예 비워서 오는 요청들이다. 이건 100% 걸러낼 수 없다. 완벽하지 않다는 걸 인정하고 넘어갔다.
유니크 방문자를 쿠키 없이 세기
조회수(PV) 다음 단계는 유니크 방문자(UV)다. 같은 사람이 같은 글을 열 번 새로고침해도 PV는 10인데 UV는 1이어야 한다. 보통은 쿠키로 해결한다. 첫 방문 때 고유 ID를 쿠키에 심고, 다음에 또 오면 "아, 아까 그 사람이구나" 하고 넘기는 식이다.
쿠키를 안 쓰기로 했으니 다른 방법이 필요했다. 결론적으로 요청의 IP 주소와 날짜를 해시한 값을 당일 중복 판별에 쓴다. 원본 IP는 저장하지 않는다. 해시만 남기고, 그 해시도 하루가 지나면 삭제된다. 누가 방문했는지는 모르지만 "오늘 몇 명이 왔는지"는 안다.
완벽한 UV 측정인가? 아니다. 같은 IP를 쓰는 사람들(회사 Wi-Fi 등)은 한 명으로 잡힌다. VPN을 쓰면 다른 사람으로 잡힐 수 있다. 하지만 블로그 분석에 소수점 단위의 정확도가 필요한 건 아니다. 대략적인 추이를 파악하는 데는 충분하다.
대시보드에 올리면서 터진 버그 하나
UV 추적을 추가하고 대시보드에 카드를 넣었는데, 바로 크래시가 났다. 원인이 황당했다. UV 기능을 붙이기 전에 캐시된 API 응답에는 UV 필드가 없었다. 대시보드 코드는 total_uv_7d 같은 값이 항상 존재한다고 가정하고 있었고, undefined에 .toLocaleString()을 호출하면서 터진 것이다.
고치는 건 ?? 0 폴백 한 줄이었지만, 교훈은 명확했다. 새 필드를 추가할 때는 캐시에 이전 버전 응답이 남아 있을 수 있다는 걸 항상 기억해야 한다. 특히 CDN 캐시가 있는 환경에서는 더 그렇다. 스키마 마이그레이션의 축소판 같은 문제다.
어디까지 추적할 것인가
Google Analytics를 쓰면 스크롤 깊이, 체류 시간, 이탈률, 사용자 흐름, 인구통계까지 볼 수 있다. Postlark의 분석은 그에 비하면 한참 초라하다. PV, UV, 날짜별 추이, 인기 글 순위. 끝이다.
그런데 블로그 운영자에게 진짜 필요한 정보가 뭘까 생각해보면, 대부분은 "내 글을 얼마나 읽고 있나"와 "어떤 글이 인기 있나" 두 가지다. 스크롤 깊이를 알아서 뭘 하겠나. 이탈률이 높다고 글 구성을 바꿀 블로거가 얼마나 될까. 대다수 블로거에게 세밀한 행동 분석은 소음이다.
덜 추적하는 것이 Postlark의 분석 철학이다. 독자의 프라이버시를 존중하면서, 블로거에게 꼭 필요한 숫자만 보여준다. 그게 JS 0바이트로 가능하다면, 굳이 더 복잡하게 갈 이유가 없다.