정적 사이트에 조회수 한 줄 붙이기 — Cloudflare 함수 하나와 D1 표 하나
서버가 없는 정적 사이트의 첫 화면에 「오늘 · 누적」 숫자를 띄우는 가장 작은 구성입니다. 표 하나에 하루 한 줄, 날짜를 서울 기준으로 미는 이유, 못 받아 왔을 때 0이라고 쓰지 않는 이유, 그리고 셈을 「방문자」에서 「조회수」로 바꾸면서 이름까지 같이 바꿔야 했던 이야기를 적었습니다.
이 사이트의 첫 화면에는 「오늘 조회수 · 누적 조회수」 두 숫자가 떠 있습니다. 사이트는 정적 파일만 올리는 곳(Cloudflare Pages)이고 서버가 없는데, 숫자를 세려면 어딘가에 기억이 있어야 합니다.
우리가 원한 것은 분석 도구가 아니었습니다. 화면에 띄울 숫자 하나였습니다. 그래서 제일 작은 구성으로 붙였습니다 — 함수 하나와 표 하나입니다.
전체 구성
조각이 셋이고, 셋이 다 있어야 숫자가 뜹니다.
| 조각 | 무엇 |
|---|---|
| 세는 곳 | 요청 하나를 받는 함수(Pages Functions) |
| 담는 곳 | D1(서버리스 SQLite) 표 하나 — hits(day, n) |
| 잇는 곳 | 설정 파일의 데이터베이스 바인딩 한 줄 |
표는 이게 전부입니다. 하루에 한 줄이고, 칸은 날짜와 횟수 둘입니다. 누적은 따로 세지 않고 SUM(n) 으로 그때그때 더합니다. 하루 한 줄이면 10년이 3,650줄이라 더하는 값이 거의 0입니다. 누적을 따로 들고 있으면 둘이 어긋날 자리가 생기는데, 그 걱정을 아예 없애는 편이 쌉니다.
올리는 것과 읽는 것을 한 번에
함수가 하는 일은 둘입니다. ?inc=1 이 붙어 오면 오늘 줄을 하나 올리고, 그다음 오늘과 누적을 읽어 돌려줍니다.
올릴 때는 있으면 더하고 없으면 만드는 SQL 한 줄로 끝납니다 — INSERT INTO hits (day, n) VALUES (?1, 1) ON CONFLICT(day) DO UPDATE SET n = n + 1. 「오늘 줄이 있나 보고, 없으면 만들고, 있으면 더한다」를 세 번 왕복으로 하면 같은 순간에 두 사람이 들어왔을 때 한 명이 사라집니다. 한 줄이면 그 틈이 없습니다.
읽을 때도 한 번입니다 — SELECT (SELECT n FROM hits WHERE day = ?1) AS today, (SELECT SUM(n) FROM hits) AS total. D1 은 왕복 한 번이 값의 대부분이라, 두 숫자를 한 번에 받는 것이 두 번 묻는 것보다 눈에 띄게 낫습니다.
응답에는 cache-control: no-store 를 답니다. 숫자가 캐시되면 누구에게나 같은 숫자가 몇 분씩 머물러서, 센 것이 안 보입니다.
날짜를 서울 기준으로 미는 이유
이게 제일 쉽게 틀리는 자리입니다. 날짜를 UTC 로 세면 우리 밤 9시에 하루가 바뀝니다. 저녁에 들어온 사람이 「내일」로 세어지고, 「오늘 조회수」가 밤 9시에 1로 돌아갑니다.
그래서 아홉 시간을 밀어서 날짜만 떼어 씁니다 — new Date(now + 9*3600*1000).toISOString().slice(0, 10). 시간대 이름을 다루는 것보다 짧고, 틀릴 자리가 없습니다(한국은 일광절약시간이 없어서 이 단순함이 성립합니다).
⚠ 날짜를 두 곳에서 센다면 두 곳이 같은 식을 써야 합니다 — 한쪽만 서울 기준이면 밤에 「오늘」이 서로 다른 날이 됩니다. 방문자를 세던 때는 화면도 「오늘 이미 셌나」를 보려고 날짜를 셌습니다. 지금은 서버 한 곳만 셉니다.
못 받아 오면 숫자를 감춘다
고장이 났을 때 0이라고 적지 않습니다. 숫자 자리를 통째로 감춥니다.
「오늘 0」은 거짓말입니다 — 지금 그 화면을 연 것부터가 하나니까요. 받아 온 값이 숫자일 때만 그 칸을 드러내고, 그 전에는 자리 자체가 없습니다. 데이터베이스 연결이 없으면 함수도 숫자를 꾸며 내지 않고 상태 코드로 거절합니다.
⚠⚠ 대신 값을 하나 치릅니다. 고장이 조용합니다. 숫자가 안 뜨는 화면은 「원래 그런 화면」처럼 보입니다. 그래서 점검에 세 조각이 다 있나를 넣어 뒀습니다(함수 파일 · 화면의 숫자 자리 · 설정의 바인딩). 셋 중 하나만 빠져도 화면은 멀쩡해 보이는데 숫자는 영영 안 뜨기 때문입니다.
💡 로컬에서는 가짜 숫자를 줍니다. 자리를 잡는지만 보면 되는 자리라서요. 진짜 숫자는 배포본에서만 나옵니다.
자동화 브라우저는 안 센다
스크린샷을 찍는 프로그램처럼 자동으로 도는 브라우저는 세지 않습니다(navigator.webdriver). 우리가 사이트를 손볼 때마다 숫자가 부풀면, 그 숫자를 보고 판단할 수 없게 됩니다. 본인 접속을 다 거를 방법은 없지만, 우리 도구가 만든 접속은 공짜로 거를 수 있습니다.
「방문자」에서 「조회수」로 — 이름까지 같이 바꿨다
처음에는 방문자를 셌습니다. 브라우저에 「오늘 이미 셌다」는 표시를 남겨 두고, 그날 처음일 때만 올렸습니다. 새로고침해도 안 오릅니다.
그런데 광고 쪽 숫자(페이지 조회수)와 셈이 달라서, 같은 날의 두 숫자를 견줄 수 없었습니다. 그래서 페이지가 열릴 때마다 하나로 바꿨습니다. 바꾸면서 같이 바꿀 것이 넷 더 있었습니다.
- 이름 — 새로고침까지 세는 숫자에 「방문자」라고 붙이면 그 이름이 거짓말입니다. 화면 표시를 「조회수」로 바꿨습니다. 셈을 바꿀 때 제일 잊기 쉬운 것이 이름입니다.
- 세는 자리 — 첫 화면만 세다가 모든 페이지가 세게 됐습니다. 숫자 자리는 첫 화면에만 있으니, 다른 페이지는 「올리기만 하고 안 그리는」 호출이 됩니다. 주소를 뿌리부터 적는 것이 여기서 중요해집니다 — 깊은 페이지에서 상대 주소로 부르면 없는 곳을 부릅니다.
- 지난 줄의 단위 — 바꾼 날 앞의 줄은 방문자고 뒤는 조회수입니다. 한 표 안에 단위가 둘 섞여 있습니다. 지울 수도 없고(그게 그날의 사실입니다) 섞어 평균을 낼 수도 없어서, 표를 읽는 코드와 설명에 그 경계를 적어 뒀습니다. 이런 건 반드시 반 년 뒤에 「9월이 왜 이렇게 적지?」로 돌아옵니다.
- 개인정보처리방침 — 방침에 「브라우저에 오늘 이미 셌다는 표시를 둔다」는 문장이 있었습니다. 그 표시를 없앴으니 방침이 사실과 달라집니다. 문장을 고치고 시행일도 같이 올렸습니다.
남는 숫자가 무엇인지
정직하게 적으면, 이건 사람 수가 아닙니다. 페이지가 열린 횟수입니다. 같은 사람의 새로고침도, 자바스크립트를 돌리는 크롤러도 들어 있습니다. 쿠키도 아이피도 남기지 않으니 사람을 가려낼 재료 자체가 없습니다 — 표에 있는 것은 날짜별 숫자 하나뿐입니다.
그걸 알고 쓰면 쓸 만합니다. 「어제보다 늘었나」, 「글을 올린 날 움직였나」 정도에는 충분하고, 그 이상을 알아야 할 만큼 큰 사이트가 되면 그때 제대로 된 도구를 붙이면 됩니다. 지금 드는 값은 함수 하나와 두 칸짜리 표 하나입니다.