방문자 수를 IP로 세면 안 되는 이유 — HyperLogLog 로 바꿨다
누적 101이 전부 내 방문이었다. IP 제외 목록은 남까지 지운다는 걸 알고 방향을 접었다.
블로그 아래에 조회수가 떠 있다면, 그 숫자가 몇 명인지 몇 번인지 구분해서 본 적이 있는지 묻고 싶다. 나는 없었다.
2026-08-05, 화면 아래에 **「누적 101」**이 떠 있었다. 발행을 시작한 게 7/30 밤이니 6일 만에 101이다. 보다가 문득 걸렸다. 이게 전부 내가 들어간 횟수 아닌가.
코드를 열어보니 그랬다. lib/views.ts 가 하는 일은 Redis INCR 한 줄이었다. IP도 쿠키도 세션도 저장하지 않으니 중복을 걸러낼 수단 자체가 없었다. 글 목록에서 글 A로 갔다가 뒤로 나와 글 B를 열면 그것만으로 4가 오른다. 경로가 바뀔 때마다 카운터가 다시 쏘는 구조였다.
그러니까 101은 「몇 명이 왔나」가 아니라 「페이지가 몇 번 열렸나」였고, 그중 상당수가 글 확인하러 들어간 나였다.
IP 제외 목록을 접은 이유
처음 잡은 방향은 **「내 방문만 빼는 스위치」**였다. 브라우저에 표시를 하나 저장해두고 그 브라우저는 안 세는 방식. 30분이면 붙는다.
그러다 다른 길이 떠올랐다. IP로 하면 안 되나?
IP로 방문자를 가리는 건 기술적으로 아무 장벽이 없다. Vercel에서는 헤더 한 줄이면 나온다. 법적으로도 처리방침에 항목을 적으면 되는 일이라, 그게 벽은 아니었다.
진짜 문제는 다른 데 있었다. IP로 「나」를 특정하는 게 안 된다.
- 집 IP는 고정이 아니다. 공유기를 껐다 켜거나 통신사가 다시 나눠주면 바뀐다
- 폰이 더 나쁘다. LTE·5G는 통신사가 수천 명을 한 IP 뒤에 몰아넣는다
- 그래서 「내 IP를 제외 목록에 넣는다」를 하면, 그 IP 뒤에 있는 모르는 사람들의 방문이 같이 사라진다
나를 빼려다 남을 지우는 구조였다. 방향을 접었다.
대신 질문 자체가 갈렸다. 「내 방문을 빼고 싶다」와 「몇 명이 왔는지 알고 싶다」는 다른 요구였고, 지금까지 하나로 뭉쳐서 생각하고 있었다.
IP를 저장하지 않고 사람 수만 센다
방문 한 건이 지나가는 길은 이렇다.
- 요청 헤더에서 IP와 브라우저 정보를 읽는다 — 메모리에서만
SHA-256(IP + 브라우저 + 한국 날짜 + 비밀값)으로 바꾼다- 그 값을 Redis HyperLogLog에 던진다 (
PFADD) - 응답이 끝나면 IP도 해시도 사라진다
HyperLogLog는 원소를 담지 않는 자료형이다. 12KB 고정 크기 확률 구조체라 「이 사람 왔었나」를 되물을 명령 자체가 없다. 명단이 아니라 개수만 남는다. 게다가 해시 재료에 날짜가 들어가서 날이 바뀌면 같은 사람도 다른 값이 된다. 어제 온 사람과 오늘 온 사람을 이을 방법이 원천적으로 없다.
그 대가로 포기한 것도 있다. 주간·월간 순 방문자는 못 센다. 일부러 그렇게 뒀다.
바뀐 파일은 넷이다.
app/api/view/route.ts— IP를 만지는 유일한 자리. 다른 데로 새면 처리방침이 거짓이 된다는 주석을 박았다lib/views.ts—PFADD/PFCOUNT를 기존 파이프라인에 얹었다(왕복은 그대로 1회)app/privacy/page.tsx— 「IP·브라우저 정보를 저장하지 않습니다」에서 **「잠깐 읽지만 그대로 저장하지 않습니다」**로docs/operations.md— 7/30에 정한 「IP를 읽지도 않는다」 결정에 취소선을 긋고 왜 좁혔는지 남겼다
그리고 내 방문 빼는 스위치도 결국 같이 넣었다. 방문자 수가 생겨도 나는 여전히 그날의 1명이고, 숫자가 작을 때 그 1이 크게 보이기 때문이다. 주소 뒤에 ?count=off 를 붙여 한 번 열면 그 브라우저는 빠진다.
뒤집은 게 아니라 좁혔다
7/30 문서에는 **「IP·UA·쿠키를 저장하지 않는다」**가 대문짝만하게 적혀 있었다. 이번 작업은 그걸 뒤집은 것처럼 보이지만 아니다. 저장은 여전히 안 한다. 풀린 건 「읽지도 않는다」 쪽이다.
문서를 고칠 때 옛 문장을 지우지 않고 취소선으로 남겼다. 지우면 **「왜 그때 그렇게 정했더라」**에 답할 수 없어서, 몇 달 뒤 같은 제안이 또 올라온다.