통계를 간단하게
원문은 Nikita Prokopov님이 에 게재했습니다. 이 블로그 구독하기
나는 통계와 묘한 관계에 있다. 한편으로는 되도록 자주 들여다보지 않으려 한다. 일 년에 한두 번 정도. 애널리틱스는 뭔가를 실행에 옮길 수 있게 해주지 않기 때문이다. 내 글을 천 명이 봤든 만 명이 봤든 무슨 차이가 있겠는가?
물론 사람들의 취향을 추측해서 인기 있는 주제만 골라 쓸 수도 있겠지만, 그러다 보면 영혼이 꽤 빨리 망가질 것이다.
반면 뭔가 집계되지 않거나, 기록되지 않거나, 나중에 참고하려고 저장되지 않으면 불안하다. 지금은 필요 없을지 몰라도 10년 뒤에 마음이 바뀌면 어쩌지?
독자가 보인다는 건 내가 허공에 대고 글을 쓰는 게 아니라는 걸 알게 해주기도 한다. 그래서 사실 많은 건 필요하지 않다. 아주 기본적인 것, 하루/글당 독자 수 정도면 충분할 것 같다.
마지막 퍼즐 조각: 나는 웹 프로젝트를 직접 호스팅하고, 그 일을 Nginx에 맡기는 대신 옛날 방식의 웹 서버를 쓴다.
정적 사이트가 인기 있는 데는 이유가 있다. 빠르고, 가볍고, 제 역할을 다한다. 반면 나는 어쩌면 끝나지 않은 게슈탈트를 한두 개 안고 있는지도 모른다. 웹 페이지를 서빙하면서 컴퓨터의 온전한 힘을 느끼고 싶고, 정적 페이지를 넘어서는 재미있는 것들을 하고 싶다. 제대로 된 프로그래밍 언어를 손에 쥐었을 때 오는 그 자유가 필요하다. 나는 내 웹 서버를 직접 프로그래밍하고 싶다(Clojure로, 다른 분들에겐 죄송).
기존 선택지
이 모든 것이 내 필요에 딱 맞는 통계 솔루션을 찾아 떠나는 여정으로 이어졌다. Google Analytics는 탈락이었다. 비대하고, 프라이버시를 존중하지 않고, UX는 끔찍하고, Google은 사악하고, 등등.

다른 JS 솔루션을 쓰는 것도 가능은 했겠지만 여전히 의문투성이었다. SaaS? 유료? 10년 뒤에도 살아남아 있을까? 셀프 호스팅? 쿠키는 GDPR을 준수할까? RSS 피드는 어떻게 집계하지?
Nginx에는 액세스 로그가 있으니, 그걸 기반으로 하는 서버 사이드 통계(이를테면 Goatcounter)를 시도해 봤다. 설정은 쉬웠지만, 그러고 나니 도메인을 만들고, 계정을 관리하고, 프로세스를 모니터링해야 했고, 내 서버/요청량에서는 성능조차 충분히 나오지 않았다!
내 해결책
결국 직접 만들게 됐다. 제약 조건이 나와 비슷하다면 함께 써도 좋다. 이렇게 생겼다:

꽤 기본적이지만, 나에게 중요했던 몇 가지는 해낸다.
설정
설정이 극도로 쉽다. 그리고 이건 장점으로서 하는 말이다.
Ring 스택에 우리 미들웨어만 추가하면 수집부터 리포팅까지 모든 게 자동으로 이루어진다.
(def app
(-> routes
...
(ring.middleware.params/wrap-params)
(ring.middleware.cookies/wrap-cookies)
...
(clj-simple-stats.core/wrap-stats))) ;; <-- just add this가장 좋은 의미에서 제로 설정이다. 설정할 것도, 모니터링할 것도 없고, 의존성도 최소다. 즉시 작동하기 시작하고, 당신에게 그 어떤 것도 요구하지 않는다, 절대.
생각해 보라, 이미 웹 서버가 있는데 어차피 해둔 설정을 재활용하면 되지 않겠는가?
요청 유형
우리는 요청 유형을 구분한다. 내 경우에는 실제 사람에게만 관심이 있어서, RSS 피드 요청, favicon 요청, 리다이렉트, 잘못된 URL, 봇과는 따로 집계한다. 요즘 봇들은 특히 활발하다. 어딘가에서 AI 학습 데이터를 긁어와야 하니까.
RSS 피드도 일종의 실제 사람이니, 제대로 집계하려고 추가 작업을 했다. 하루에 같은 독자가 feed.xml을 100번 요청해도 한 번으로만 집계된다.
호스팅형 RSS 리더들은 종종 User-Agent에 사용자 수를 표시한다, 이렇게:
Feedly/1.0 (+http://www.feedly.com/fetcher.html; 457 subscribers; like FeedFetcher-Google)
Mozilla/5.0 (compatible; BazQux/2.4; +https://bazqux.com/fetcher; 6 subscribers)
Feedbin feed-id:1373711 - 142 subscribers이 목록에 있는 모든 분들께 개인적인 존경과 감사를 전한다. 잘 보고 있다.

그래프
시각화는 중요하고, 올바른 그래프 유형을 고르는 것도 중요하다. 이건 잘못된 예다:

연속된 선은 보간을 암시한다. 새벽 5시에 1회 방문과 6시에 11회 방문 사이에 2, 3, 5, 9회 방문이 있었던 지점들이 있었다는 것처럼 읽힌다. 심지어 5.5회 방문까지도! 그렇지 않다.
의미적으로 올바른 버전의 그래프는 이렇게 생겨야 한다:

축 레이블이 합리적이도록 하는 데도 신경을 썼다. 117, 234, 10875 같은 숫자는 보이지 않을 것이다. 우리는 항상 스케일에 맞는 둥근 숫자를 선택한다. 100, 200, 500, 1K 등.
모든 그래프가 동일한 세로 스케일을 갖고 가로 스크롤이 동기화된다는 건 말할 필요도 없다.
인사이트
많이 제공하지는 않는다(나도 많이 필요하지 않으니), 하지만 페이지, 쿼리, 리퍼러, 유저 에이전트, 그리고 특정 날짜 구간으로 리포트를 좁혀 볼 수 있다.
아직 구현되지 않은 것들
“이 스파이크는 무엇 때문에 생겼을까?”에 대한 인사이트가 있으면 좋겠다.
국가별 기본 분석도 있으면 좋겠다. IP 주소는 가지고 있긴 한데(그게 얼마나 쓸모가 있을지는 몰라도), GeoIP를 합리적인 크기(가능하면 1Mb 미만, 해상도가 좀 떨어져도 괜찮다)로 패키징할 방법이 필요하다.
마지막으로, 정말 궁금한 것 중 하나는 “누가 나에 대해 썼을까?”이다. 리퍼러는 가지고 있으니, 문제는 어떻게 노이즈 속에서 시그널을 걸러내느냐다.
성능. DuckDB는 스포츠 같은 것이다. 데이터를 압축하고 컬럼 쿼리를 실행하니, 행마다 컬럼을 추가해도 쿼리 성능에 영향을 주지 않는다. 그럼에도 대시보드를 열 때마다 전체 데이터베이스에 대해 쿼리를 실행하는데, 현재 시점(약 3년치 데이터)에는 용량이 600 MiB 정도 된다. 미리 계산된 집계를 만드는 걸 확실히 알아봐야 한다.
언젠가는.
이용 방법
github.com/tonsky/clj-simple-stats에 가서 안내를 따르면 된다:

어떻게 생각하는지 알려 달라! 쓸 만한가? 무엇을 개선하면 좋을까?
글을 무작위로 읽기
댓글
로그인하고 댓글 남기기