STL 파일을 올리면 3D 프린팅 시간·재료·비용을 계산하는 브라우저 도구를 만들었다
STL 파일을 서버로 올리지 않고 브라우저 안에서 분석해 3D 프린팅 예상 시간, 필라멘트 사용량, 비용과 출력 주의사항을 계산하는 도구를 만들었다.
블로그 목록에 보이는 조회수는 꽤 단순한 숫자처럼 보인다. 하지만 그 숫자를 어디서 가져오고, 무엇을 하나의 조회로 볼지 결정하는 순간부터 생각보다 많은 선택이 생긴다.
이 블로그는 한동안 GA4 Data API에서 screenPageViews를 읽어 GitHub Actions 빌드 과정에 넣는 방식을 사용했다. GA4는 검색 유입, 방문 경로, 체류시간처럼 콘텐츠를 개선하는 데 필요한 분석에는 여전히 좋은 도구다.
다만 화면에 보여 줄 “이 글은 얼마나 읽혔나”라는 숫자에는 다른 기준이 필요했다.
GA4가 반환한 숫자가 오류라는 뜻은 아니다. 설정된 속성으로 수집된 페이지뷰를 잘 보여 준다. 문제는 그 숫자의 의미가 사이트에서 기대하는 공개 조회수와 정확히 같지 않다는 데 있었다.
예를 들어 /article/글-이름과 /article/글-이름/은 사람에게는 같은 페이지다. 집계 코드에서 정규화할 수는 있지만, URL 변경 이력 전체를 분석 도구에만 맡기기는 어렵다.
그래서 GA4는 분석용, 사이트 안의 조회수는 서비스 기능용으로 역할을 나누기로 했다.
블로그 본문과 배포는 계속 GitHub Pages와 Astro를 사용한다. 대신 조회수처럼 새로고침 없이 변하는 기능만 Cloudflare에 분리한다. 블로그 호스팅을 옮기는 것이 아니라, 조회수 API만 별도 Worker로 운영하는 방식이다.
GitHub Pages / Astro
├─ 글을 열면 조회 이벤트 전송
└─ 글별 통계 조회
│
▼
Cloudflare Worker
├─ 요청 검증과 중복 제한
└─ Cloudflare D1에 집계 저장
Worker는 요청이 있을 때만 실행되고, D1은 이 프로젝트에 필요한 단순 SQL 집계에 잘 맞는다. 무엇보다 이 기능을 위해 블로그 전체를 다른 호스팅으로 옮길 필요가 없었다.
배포도 분리한다. 기존 GitHub Actions는 Astro 정적 결과물을 GitHub Pages에 배포하고, Worker 변경이 있을 때만 Wrangler가 Cloudflare Worker를 배포한다. 같은 저장소에 있어도 서로의 런타임이나 배포 결과를 바꾸지 않는다.
전환 직전의 GA4 JSON은 65개 글 경로로 정규화해 D1 baseline에 한 번 import했다. 결과는 65개 쿼리, 130개 행 쓰기였고 D1 크기는 약 0.06MB였다.
이 기준값은 이후 Worker가 직접 센 조회와 읽기 API에서 합산한다. 원본은 별도 테이블에 남겨 두 집계의 출처를 구분한다. 그래서 전환일에 카드 숫자가 0으로 떨어지지 않고, baseline import를 재시도해도 자체 집계가 중복되지 않는다.
브라우저는 글의 content ID만 API에 보낸다. Worker가 이를 /article/{id}로 정규화하고, 목록 페이지는 여러 ID를 한 번의 bulk 요청으로 받아 갱신한다.
이 글을 쓰는 시점에는 Worker/D1과 baseline import를 마쳤고, Astro의 수집·표시 코드는 운영 배포와 실제 브라우저 검증을 앞두고 있다. 따라서 아직 “이미 실시간 조회수가 공개됐다”라고 말할 단계는 아니다.
실제 전환 과정에서는 여기서 한 번 멈췄다. 첫 GA4 snapshot에는 65개 글만 있었는데, GitHub Actions가 다음 정적 배포에서 더 최신 GA4 조회수를 다시 받아 카드에 넣었다. 브라우저는 이후 D1 API 값을 우선 표시하므로, snapshot에 없던 기존 글은 카드에서 0으로 바뀌었다.
Worker나 D1이 조회수를 지운 것은 아니었다. 기존 GA4 누적값을 옮기는 snapshot이 불완전했던 것이 원인이었다. 처음에는 누락 글을 자체 집계 시작점으로 둘 수도 있다고 생각했지만, 이 프로젝트의 목적은 기존 공개 조회수를 이어 가는 것이므로 맞지 않는 선택이었다.
해결은 현재 GA4 전체 값을 그대로 다시 import하는 것이 아니었다. 그러면 전환 후의 GA4 조회와 Worker 자체 집계가 겹칠 수 있다. 대신 전환일 전날까지의 GA4 값을 다시 조회하고, D1에 아직 없는 경로만 추가하는 missing-only 보완 import를 만들었다.
전환일 이전 GA4 값 → 없는 baseline 경로에만 추가
전환 후 Worker 조회 → 기존 자체 집계에만 유지
이 절차는 기존 baseline과 전환 후 조회를 덮어쓰지 않는다. 숫자 이전에서는 한 번의 성공 로그보다, 어떤 날짜까지의 숫자인지와 재실행했을 때 무엇이 바뀌지 않는지를 명확히 하는 편이 더 중요하다는 것을 배웠다.
Cloudflare에는 api.yeppyshiba.com만 Worker의 Custom Domain으로 연결했다.
blog.yeppyshiba.com → GitHub Pages의 정적 Astro 사이트
api.yeppyshiba.com → Cloudflare Worker → D1
api.yeppyshiba.com은 *.yeppyshiba.com Universal 인증서 범위에 들어간다. 반면 api.blog.yeppyshiba.com처럼 두 단계 아래 hostname은 별도 인증서가 필요할 수 있다. Custom Domain을 사용하면 Cloudflare가 필요한 DNS 레코드와 TLS 인증서 발급을 관리하므로 같은 이름의 CNAME을 따로 만들 필요가 없다.
이 구분 덕분에 조회수 API에 문제가 생겨도 정적 글 본문을 제공하는 GitHub Pages 배포에는 영향을 주지 않는다. 반대로 GitHub Pages를 새로 배포해도 Worker와 D1의 조회수는 유지된다.
조회수는 새로고침 횟수가 아니다. 이 블로그에서는 다음 기준을 사용한다.
이것은 완벽한 순 방문자 수를 만들려는 기준이 아니다. 로그인 없이 개인을 정확하게 구분하는 일은 이 블로그에 필요하지 않다. 대신 반복 새로고침이나 단순 자동 요청으로 공개 숫자가 쉽게 흔들리지 않게 하는 기준이다.
같은 브라우저의 같은 날 중복을 제한하려면 서버는 적어도 같은 브라우저가 다시 요청한 것인지는 알아야 한다.
처음에는 localStorage에 UUID를 저장하는 방법도 생각했다. 구현은 쉽지만 JavaScript에서 값을 읽고 바꾸기 쉽다. 대신 API가 불투명한 랜덤 값을 담은 HttpOnly 쿠키를 발급하도록 했다. JavaScript는 이 값을 읽을 수 없고, 브라우저가 API 요청에만 보낸다.
데이터베이스에는 쿠키 원본을 저장하지 않는다. 서버만 아는 비밀 값으로 만든 HMAC 해시만 저장한다.
익명 쿠키 + 날짜를 해시해 같은 날의 중복만 막는다.쿠키를 지우거나 다른 기기에서 접속하면 새 익명 사용자로 보인다. 더 강한 식별을 위해 개인 데이터를 늘리기보다, 가벼운 반응 기능에는 이 정도의 익명성이 적절하다고 판단했다.
공개 조회수를 이전해도 GA4를 없애지는 않는다. GA4는 어떤 검색어와 채널에서 방문했는지, 어떤 글에서 다음 글로 이동했는지, 어느 콘텐츠의 체류시간이 긴지를 보는 분석 도구다. 자체 집계는 사이트에 표시할 조회수를 안정적으로 운영하는 기능이다.
둘의 숫자가 항상 같을 필요도 없다. 수집 기준이 다르기 때문이다. 우선은 전환 직전 GA4 기준값과 자체 집계를 안전하게 잇고, 실제 운영 검증을 마치면 조회수 UI에서 빌드 시점 GA4 fallback도 제거할 예정이다.
처음에는 조회수 이전과 집계만 다룬다. 충분히 안정적으로 운영되면 “도움됐어요” 같은 가벼운 반응이나 오탈자·링크 오류 제보도 같은 작은 백엔드 위에 얹을 수 있다. 하지만 시작은 작게 하려고 한다. 공개 숫자는 정확한 척하는 것보다, 무엇을 세고 무엇을 세지 않는지 분명한 편이 더 낫기 때문이다.
STL 파일을 서버로 올리지 않고 브라우저 안에서 분석해 3D 프린팅 예상 시간, 필라멘트 사용량, 비용과 출력 주의사항을 계산하는 도구를 만들었다.
Next.js 기반 정적 블로그를 Astro + Tailwind로 이주하면서 얻은 것들(속도, 단순함, 디자인 시스템).
gatsby 는 정적 사이트 생성기로써, 훌륭한 블로그 툴입니다. 다만 조회수등 여러가지 다이나믹한 기능들은 (바로) 지원하지 않는데요...
github page 기능을 이용해서 수많은 사람들이 블로그로 애용 했습니다. 그렇지만 나는 이미지 파일 하나 올릴껀데? jekyll 블로그를 써야해? 라는 사람이 있겠죠? 그런 사람을 위해서 준비했습니다!