도메인 하나로 개인 사이트를 무료로 운영하기

구름 하나 위에 작은 도서관과 자물쇠 달린 서랍장과 방문객 개찰구가 올라가 있고 구름 아래에 빈 가격표가 매달려 있다
가격표는 비어 있는데 동네는 꽤 붐빈다

정적인 블로그를 무료로 올리는 방법은 이미 많다. 그럼 남에게 보여 주기 싫은 노트 몇 개만 로그인 뒤에 숨기는 건 어떨까. 바닥글에 방문자 수를 찍으려고 데이터베이스까지 붙이는 건?

내 사이트 graphite.is 는 이걸 전부 클라우드플레어 무료 플랜 한 계정에서 한다. 쓰는 건 DNS, SSL 인증서, Workers 정적 자산, Access, D1 다섯 가지이고 블로그와 문서와 비공개 노트와 방문자 수가 그 위에서 돌아간다. 무료 한도는 이렇다.

서비스용도무료 한도
DNS, SSL도메인 연결, HTTPS 인증서추가 요금 없음
Workers 정적 자산HTML, 이미지, 스크립트 응답요청 수 제한 없음. Worker 버전당 파일 20,000개, 파일당 25MiB
Workers 스크립트301 이동, API하루 100,000 요청, 요청당 CPU 10ms
D1방문자 수 같은 작은 데이터하루 읽기 500만 행, 쓰기 10만 행, 저장 5GB
Access일부 경로 로그인사용자 50명까지

어느 요청이 어느 한도에 잡히는지는 따로 봐야 한다. 정적 자산 요청은 Workers 스크립트의 요청 한도에 들어가지 않는다. HTML 과 이미지를 그대로 내주는 동안에는 요청 수를 셀 일이 없고 301 이동이나 API 처럼 스크립트를 거치는 요청만 하루 100,000 요청 쪽으로 잡힌다. 스크립트를 거치는 요청을 줄이는 설정은 Next.js 정적 사이트의 Workers 이전 기록에 적어 두었다.

도메인 연결

사용자 지정 도메인을 쓰려면 먼저 도메인을 클라우드플레어의 활성 영역(zone)으로 추가해야 한다. 대시보드에서 도메인을 추가하고 도메인을 산 등록 기관에 가서 네임서버를 클라우드플레어가 안내한 두 개로 바꾼다. 대시보드에서 도메인이 활성 상태로 바뀌면 wrangler.jsonc 의 routes 에 도메인을 custom_domain 으로 넣고 배포한다. 나는 graphite.is 와 www.graphite.is 두 개를 넣었다.

wrangler.jsonc
{
  "routes": [
    { "pattern": "graphite.is", "custom_domain": true },
    { "pattern": "www.graphite.is", "custom_domain": true }
  ]
}

배포하면 DNS 레코드와 인증서는 클라우드플레어가 만든다. HTTPS 를 위해 따로 할 일이 없고 표에 적힌 대로 추가 요금도 없다.

걸리는 곳은 하나다. 같은 이름의 CNAME 레코드가 이미 있으면 사용자 지정 도메인을 만들 수 없다. 그런 레코드가 남아 있다면 먼저 지우고 배포해야 한다.

일부 경로만 로그인 뒤에 두기

사이트는 다 열어 두고 몇 개 경로만 로그인 뒤에 두고 싶을 때가 있다. 내 경우에는 방송대 노트와 학습 기록이 그랬다. 이 일은 Zero Trust 의 Access 가 맡는다. 자체 호스팅 애플리케이션을 만들고 애플리케이션 도메인 뒤에 막을 경로를 붙이면 된다.

항목값
애플리케이션 도메인graphite.is/private*, www.graphite.is/private*
정책허용(Allow), 포함 조건은 본인 이메일 하나
로그인 방법이메일 일회용 PIN

허용 정책에 본인 이메일 하나만 넣었으니 그 경로를 열 수 있는 사람은 나 혼자다. 무료 플랜 한도는 사용자 50명이다. 방송대 노트와 학습 기록은 각각 경로 단위 Access 애플리케이션 뒤에 있고 사이트 코드에는 로그인 기능이 아예 없다. 로그인 확인은 전부 Access 가 요청을 Worker 로 넘기기 전에 끝난다.

같은 파일을 여는 주소가 여러 개라는 점은 따로 챙겨야 한다. 이미지나 PDF 처럼 Worker 를 거치지 않는 파일은 www 주소에서도 바로 응답된다. 그래서 www 주소도 애플리케이션 도메인에 넣어야 한다. graphite.is 만 넣어 두면 같은 파일이 www 주소에서는 로그인 없이 열린다.

실제로 내가 놓친 건 workers.dev 주소였다. Access 대상 주소가 graphite.is 였는데, workers.dev 주소로 연 같은 파일은 로그인 없이 열렸다. wrangler 설정에서 workers_dev 와 preview_urls 를 false 로 두어 workers.dev 주소와 미리보기 주소를 함께 껐고, 그 뒤로는 같은 파일이 graphite.is 주소로만 열린다.

D1 로 방문자 수 세기

정적 사이트에 방문자 수를 붙이려면 숫자를 적어 둘 곳이 있어야 한다. 클라우드플레어에서는 D1 데이터베이스를 만들어 Worker 에 연결하면 된다. 나는 데이터베이스를 만들고 날짜마다 방문자 수를 한 줄씩 적는 테이블 하나를 마이그레이션으로 올렸다.

터미널
npx wrangler d1 create graphite-stats
npx wrangler d1 migrations apply graphite-stats --remote
migrations/0001_visits.sql
CREATE TABLE IF NOT EXISTS daily (day TEXT PRIMARY KEY, visitors INTEGER NOT NULL DEFAULT 0);

Worker 쪽은 그날 줄이 없으면 1로 새로 쓰고, 있으면 1을 더한다.

worker/redirect.js
await env.STATS.prepare(
  "INSERT INTO daily (day, visitors) VALUES (?, 1) ON CONFLICT(day) DO UPDATE SET visitors = visitors + 1",
)
  .bind(day)
  .run();

브라우저는 하루에 한 번만 /api/visit 을 보내고 같은 날 다시 열면 조회만 한다. 방문 한 번에 쓰기가 1행이니 하루 쓰기 한도 10만 행과는 거리가 멀다.

읽기 쪽은 조금 따져 볼 만하다. 누적 합계를 내려면 날짜 행을 모두 읽어야 해서 1년 뒤에는 한 번에 약 370행을 읽게 된다. 그래도 하루 1만 3천 번까지는 읽기 한도 안이다. 그보다 많이 읽히기 전까지는 집계 방식을 바꿀 필요가 없다.

누적 합계에는 시작값이 있다. 티스토리 시절 누적 방문자 23,306명을 meta 테이블에 시작값으로 넣어 두고 합계에 더한다. 블로그를 옮겼어도 바닥글의 누적 숫자는 0부터 다시 세지 않는다.

옮기는 순서

도메인이 하나 있다면 한번 옮겨 보길 권한다. 네임서버를 바꾸는 데서 시작해 정적 파일부터 올리고, 숨길 경로가 생기면 Access 를, 숫자를 남기고 싶어지면 D1 을 붙이면 된다. 한도와 요금은 Workers 요금과 Zero Trust 요금에서, Access 애플리케이션 만드는 법은 자체 호스팅 애플리케이션 문서에서 확인할 수 있다.

Access 를 붙였다면 workers.dev 주소와 www 주소로 같은 파일을 열어 로그인 화면이 뜨는지 확인한다.