[연재] 개인정보처리방침, 안 쓰면 광고를 못 답니다
제 사이트 맨 아래가 거짓말을 하고 있었습니다
7월 22일 저녁 7시 53분, 홈페이지 첫 커밋을 넣었습니다. 그날 만든 페이지 맨 아래에는 이런 문장이 붙어 있었어요.
FOOT = ('<footer>관리자 <b>Victor</b> · 문의는 우상단 <b>신고·제안</b>으로 남겨주세요.<br>'
'전부 정적 페이지로 만들었습니다. 서버도 데이터베이스도 쓰지 않습니다.</footer>')
"서버도 데이터베이스도 쓰지 않습니다." 쓸 때는 이게 자랑이었어요. 글도 화면도 전부 미리 구워둔 파일이고, 뒤에서 도는 게 아무것도 없다는 뜻이니까요. 실제로 그때는 사실이었고요.
그런데 그날 밤 기록을 보면 이렇습니다.
07-22 19:53 본진(개인 홈페이지) 초안 ← 위 푸터가 들어간 커밋
07-22 20:31 익명 댓글 추가
07-22 21:29 글마다 조회수·댓글 수
댓글을 받으려면 어딘가에 저장을 해야 하고, 조회수를 세려면 더 저장을 해야 합니다. 둘 다 Supabase라는 외부 데이터베이스에 들어갔어요. 푸터를 쓴 지 38분 만에 그 문장은 거짓이 된 겁니다.
더 나쁜 건 그다음이에요. 그 문장은 이틀을 더 그 자리에 붙어 있었습니다. 7월 24일 오후 4시 31분, 개인정보처리방침 페이지를 만들면서야 지웠어요.

지금은 이렇게 적혀 있습니다. "글과 화면은 정적 파일로 만듭니다. 댓글과 조회수만 외부 데이터베이스에 저장합니다 — 개인정보처리방침."
그 자리에 지금 이런 주석이 달려 있어요.
# 예전 푸터에는 "서버도 데이터베이스도 쓰지 않습니다"라고 적혀 있었다.
# 댓글과 조회수를 붙이면서 그 말이 사실이 아니게 됐다(Supabase 에 저장된다).
# 개인정보처리방침에 저장한다고 써놓고 푸터에서 아니라고 하면 앞뒤가 맞지 않는다.
이 세 줄이 이 편 전체입니다. 개인정보처리방침이라는 게 뭐냐면, 내 사이트가 실제로 무엇을 저장하는지 적는 문서예요. 법률 문서가 아니라 사실 확인 문서입니다. 그리고 그 문서를 쓰다 보면 사이트 다른 곳에 적어둔 말이 그 사실과 어긋나 있다는 걸 알게 돼요. 저는 푸터에서 걸렸습니다.
이 문서가 없으면 광고를 못 답니다
왜 갑자기 방침 얘기냐면, 이건 광고 얘기이기 때문이에요.
지난 편들에서 쿠팡 파트너스 배너를 달았고, 애드센스를 신청했다가 두 번 떨어졌습니다. 그 과정에서 확실해진 게 하나 있어요. 광고를 붙이는 순간 이 문서가 필수가 됩니다. 코드에 이렇게 적어뒀어요.
# 광고(쿠팡·애드센스)를 붙이려면 이 문서가 있어야 한다 — 광고 쿠키를 쓴다는
# 사실을 방문자에게 알리는 것이 승인 조건이다. 그와 별개로 이 사이트는 실제로
# 댓글·계정·문의를 받으므로 국내법상으로도 필요하다.
#
# 여기 적힌 것은 전부 코드에서 확인한 실제 흐름이다. 흐름이 바뀌면 (테이블을
# 추가하거나 외부 서비스를 하나 더 붙이면) 이 표도 같이 고쳐야 한다.
# 지키지 못할 약속은 쓰지 않는다.
이유가 두 갈래예요.
하나는 광고 쪽 조건입니다. 광고는 쿠키를 씁니다. 누가 배너를 봤고 누가 눌렀는지를 구분해야 수수료 정산이 되니까요. 그 쿠키를 쓴다는 사실을 방문자한테 알려야 하고, 알리는 자리가 바로 이 문서예요. 심사에서도 봅니다.
다른 하나는 제 사이트가 실제로 뭔가를 받고 있기 때문입니다. 광고가 없어도 필요했다는 얘기예요. 저는 익명 댓글을 받고, 조회수를 세고, 카드 혜택 도구에서는 이메일과 비밀번호로 계정을 만들게 하고, 신고·제안 폼으로 내용과 회신 이메일을 받습니다. 이건 이미 남의 정보를 받는 사이트입니다.
정리하면 이래요. 광고가 방침을 요구한 건 맞지만, 광고가 없었어도 필요했습니다. 광고 덕분에 미룰 수 없게 됐을 뿐이에요.

전체 화면은 이렇게 생겼습니다. 여섯 개 절이고, 맨 위에 "최종 수정 2026-07-23"이 붙어 있어요. 이 문서를 만드는 코드는 build.py 안에 61줄로 들어 있습니다. 전체 1,703줄짜리 파일에서 1118줄부터 1178줄까지예요.
남의 방침을 베껴 오면 안 되는 이유
"개인정보처리방침 양식"으로 검색하면 완성된 문서가 수십 개 나옵니다. 이름과 사이트 주소만 바꾸면 되게 생겼어요. 저도 처음엔 그 생각을 했습니다.
안 되는 이유가 명확해요. 남의 방침에는 제가 안 하는 게 잔뜩 적혀 있습니다.
양식으로 도는 문서에는 보통 이런 게 있어요. 회원가입 시 수집하는 이름·생년월일·휴대전화번호, 서비스 이용 기록과 접속 로그의 보관 기간, 개인정보 보호책임자의 직위와 연락처, 위탁업체 목록, 파기 절차와 파기 방법, 만 14세 미만 아동에 관한 조항.
제 사이트는 저 중 대부분을 안 합니다. 생년월일을 받은 적이 없고 휴대전화번호도 안 받아요. 그런데 그게 적혀 있으면 어떻게 될까요?
없는 걸 있다고 적은 게 됩니다. 이건 실수가 아니라 거짓 고지예요. "저희는 여러분의 휴대전화번호를 안전하게 관리합니다"라고 써놓고 받지도 않는 건, 착한 거짓말이 아니라 그냥 이 문서를 읽어보지도 않고 붙였다는 증거입니다. 반대 방향은 더 나쁘고요. 실제로 저장하는데 표에 없으면 그건 안 알린 겁니다.
그래서 방향을 뒤집었어요. 양식에서 시작해서 제 사이트에 맞게 지우는 게 아니라, 제 코드에서 시작해서 실제로 저장하는 것만 적기로 했습니다.
코드에서 뽑아 적었더니 다섯 줄이었습니다
결과가 이 표예요. 방침 1절 "무엇을 받고 어디에 두는가"입니다.

다섯 줄입니다. 한 줄씩 어디서 나왔는지 말씀드릴게요. 전부 제가 짠 코드나 스키마에서 직접 확인한 겁니다.
① 댓글 — 이름(선택), 내용, 작성시각 · Supabase.
댓글 테이블을 만든 SQL이 이렇게 생겼어요.
create table if not exists public.comments (
id bigserial primary key,
post_slug text not null,
author text,
body text not null,
created_at timestamptz not null default now(),
...
);
칼럼이 다섯 개고, 그중 사람한테서 오는 건 author(이름), body(내용), created_at(작성시각) 셋입니다. 그래서 표에 그 셋만 적었어요. 표를 보고 코드를 쓴 게 아니라 코드를 보고 표를 썼습니다.

댓글창은 이렇게 생겼어요. 로그인이 없고, 이름 칸을 비우면 익명으로 남습니다.
② 조회수 — 글별 조회 횟수(개인 식별 안 함) · Supabase.
조회수는 post_views 테이블에 글 주소별 숫자 하나만 올립니다. 누가 봤는지는 아예 안 담아요. 담을 칸이 없습니다.

글 제목 밑에 "조회 2"라고만 뜨는 그 숫자예요.
③ 카드 혜택 로그인 — 이메일, 비밀번호(암호화), 담은 카드 목록 · Supabase.
제 도구 중에 로그인이 있는 건 카드 혜택 하나뿐입니다. 카드 1,180장 중에 내가 가진 걸 지갑에 담아두는 기능이 있는데, 폰에서 담은 걸 PC에서도 보려면 계정이 필요하거든요.


"내 지갑" 탭을 누르면 이 화면이 나와요. 이메일과 비밀번호를 받죠. 그래서 표에 그렇게 적었습니다.
④ 신고·제안 — 보낸 내용, 회신 이메일(선택) · Web3Forms.
화면 오른쪽 위에 노란 버튼이 있어요. 오탈자나 틀린 계산을 알려주는 창입니다.

받는 건 유형, 내용, 회신 이메일(선택) 셋이고, 이건 제 데이터베이스가 아니라 Web3Forms라는 폼 서비스를 거쳐 메일로 옵니다. 그래서 보관처를 Supabase가 아니라 Web3Forms로 적었어요. 같은 사이트라고 다 같은 데로 가는 게 아니니까요.
⑤ 접속 — IP, 접속 기록 · Cloudflare.
이건 제가 만든 게 아닙니다. 사이트를 Cloudflare에 올려두면 접속 기록이 거기 남아요. 제가 안 만들었어도 남는 건 남는 거니까 적었습니다. 보관 기간도 제가 정하는 게 아니라 "Cloudflare 정책에 따름"이라고 썼고요.
표에서 제일 오래 붙잡은 칸
다섯 줄 중에 제일 오래 붙잡은 건 표가 아니라 표 밑의 한 줄이었어요.
댓글은 로그인 없이 쓸 수 있습니다. 이름을 비우면 익명으로 남습니다. IP 주소는 댓글과 함께 저장하지 않습니다.
마지막 문장이요. "IP 주소는 댓글과 함께 저장하지 않습니다."
이걸 쓸 수 있으려면 진짜로 안 저장해야 합니다. 위에 붙인 SQL을 다시 보시면 comments 테이블에 IP를 담을 칸이 없어요. 그래서 쓸 수 있었습니다. 만약 스팸 잡겠다고 IP 칸을 하나 만들어뒀다면 이 문장은 못 씁니다.
여기서 재밌는 게, 스팸 방어를 IP 없이 해야 했다는 점이에요. 로그인도 없고 IP도 안 남기면 도배를 어떻게 막나요. 이렇게 막았습니다.
-- 봇의 가장 흔한 패턴은 같은 문구를 연속으로 뿌리는 것이다.
-- 10분 안에 같은 본문이 또 들어오면 막는다. 사람이 같은 말을
-- 10분 안에 두 번 쓰는 일은 드물다.
내용이 같으면 막고, 길이 제한을 걸고(2~1000자), 링크가 세 개 넘으면 막습니다. 누가 썼는지 대신 무엇을 썼는지로 막은 거예요. 사람을 식별하지 않기로 했으니 그 방향밖에 안 남았고, 그게 결과적으로 방침을 한 줄 더 깔끔하게 만들어줬습니다.
방침이 코드를 따라가기만 한 게 아니라, 방침을 쓸 수 있게 코드를 그렇게 짠 셈이에요. 순서가 앞뒤로 오갑니다.
광고와 쿠키는 이렇게 적었습니다
2절이 광고 얘기입니다. 여기가 이 문서를 쓰게 만든 직접적인 이유고요.
세 덩어리로 적었어요.
쿠팡 파트너스 배너가 있고, 쿠키를 쓸 수 있고, 수수료를 받습니다. 수수료를 받는다는 말을 반드시 넣어야 합니다. 이건 광고 문구가 아니라 이해관계 고지예요.
애드센스가 붙을 수 있고, 거부하는 방법이 여기 있습니다. Google 광고 설정과 aboutads.info 링크를 걸어뒀어요. "쿠키를 씁니다"까지만 적고 끄는 법을 안 적으면 반쪽입니다.
유럽·영국에서 들어오시면 동의 창이 먼저 뜨고, 나중에 마음을 바꿀 수 있습니다. 화면 왼쪽 아래 "개인정보 설정" 버튼을 누르면 동의 창이 다시 열려요. 그리고 이 문장을 붙였습니다 — "동의하지 않으셔도 글과 도구는 그대로 쓰실 수 있습니다."
이 마지막 문장이 중요해요. 동의 안 하면 못 들어오는 사이트가 아니라는 뜻이고, 실제로 그렇게 만들어야 이 문장을 쓸 수 있습니다. 제 사이트는 글도 도구도 전부 미리 구워둔 파일이라 광고가 꺼져도 그냥 열려요. 그래서 쓸 수 있었습니다.
마지막에 이 한 줄을 더 붙였고요. "브라우저 설정에서 쿠키를 차단해도 글을 읽는 데는 지장이 없습니다. 다만 카드 혜택 도구의 로그인 유지가 풀립니다." 불편해지는 지점을 정확히 하나 짚어준 거예요. "일부 기능이 제한될 수 있습니다" 같은 문장은 아무 정보도 안 줍니다.
지킬 수 있는 약속만 적기
3절부터 6절은 짧습니다.

여기서 제일 마음에 드는 건 4절이에요.
본인의 댓글이나 계정을 지우고 싶으시면 화면 우상단 신고·제안 버튼으로 알려주세요. 확인 후 지워드립니다. 댓글은 로그인 없이 쓰는 구조라 본인이 직접 지우는 기능은 없습니다 — 대신 요청을 받아 처리합니다.
"본인이 직접 지우는 기능은 없습니다"를 그대로 적었어요.
이게 좀 부끄러운 문장이잖아요. 보통 이런 건 "삭제 요청은 언제든 가능합니다" 정도로 얼버무리고 싶죠. 그런데 왜 없냐면, 로그인이 없으니까 그 댓글이 그 사람 것인지 확인할 방법이 없어서예요. 데이터베이스 권한도 그렇게 걸어뒀습니다. 읽기와 쓰기만 열려 있고 수정·삭제 정책은 아예 안 만들었어요. 그래야 남이 내 댓글을 못 건드립니다.
즉 남을 못 지우게 만든 대가로 나도 못 지우게 된 구조예요. 이걸 숨기면 나중에 "왜 삭제 버튼이 없냐"는 소리를 듣고, 적어두면 처음부터 알고 쓰시게 됩니다.
5절 안전조치도 같은 원칙으로 썼어요. HTTPS만 쓴다, 데이터베이스에 행 수준 보안이 걸려 있어 남의 지갑이나 계정은 조회되지 않는다, 비밀번호는 원문을 저장하지 않는다. 셋 다 지금 코드에서 그렇게 돼 있는 것만 적었습니다. "최선을 다해 보호하고 있습니다" 같은 말은 뺐어요. 그건 검증이 안 되는 문장이니까요.
6절은 한 줄입니다. "기능이 바뀌어 수집하는 정보가 달라지면 이 문서를 먼저 고칩니다." 먼저, 라는 말을 넣은 게 저한테 하는 약속이에요.
그리고 광고를 달면 이 문서로 가는 길이 자동으로 생깁니다. 도구 화면 아래 붙는 쿠팡 고정 바를 보시면요.

"이 사이트는 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다 · 개인정보처리방침." 광고 문구 옆에 방침 링크가 나란히 붙어 있어요. 광고와 이 문서는 한 세트로 다닙니다.
그런데 표에 아직 없는 칸이 있었습니다
여기까지 쓰고 표를 다시 훑었어요. 그러다 걸렸습니다.
표의 "보관처" 칸을 보시면 Supabase, Web3Forms, Cloudflare 셋뿐이에요. 전부 제 쪽에 있는 곳입니다. 그런데 제 사이트는 읽는 분 브라우저에도 뭔가를 남겨요.
조회수 코드가 이렇게 생겼거든요.
/* 조회수 — 같은 사람이 새로고침할 때마다 오르면 숫자가 무의미해진다.
같은 글은 6시간에 한 번만 센다. */
var k = 'pv:' + SLUG, last = Number(localStorage.getItem(k) || 0);
"6시간에 한 번만 센다"를 하려면 마지막으로 센 시각을 어딘가 기억해야 합니다. 그걸 제 서버에 두면 사람을 식별해야 하니까, 반대로 읽는 분 브라우저에 뒀어요.

왼쪽 아래 "오늘 · 누적" 방문자 수도 같은 방식입니다. 오늘 날짜가 브라우저에 표시돼 있으면 안 올리고, 없으면 한 번 올리고 표시를 남겨요. 하루에 한 번만 세려고요.
실제로 글 하나를 열고 브라우저 저장소를 열어보면 이렇게 들어 있습니다.
pv:260819-연재-배포-스크립트-6개를-1개로 → 1787127891101
visit:2026-08-19 → 1
두 줄이에요. 어느 글을 언제 봤는지, 오늘 들어왔는지. 이름도 아이디도 아니고, 제 쪽으로 오지도 않습니다. 그 브라우저 안에만 있어요. 설계로 보면 이게 더 조심스러운 쪽입니다 — 사람을 식별 안 하려고 일부러 이렇게 만든 거니까요.
그런데 표에는 이 줄이 없습니다. 2절에서 쿠키 얘기는 하지만 그건 광고 쿠키 얘기고, 조회수·방문 수를 세느라 브라우저에 남기는 값은 어디에도 안 적혀 있어요.
이게 딱 이 편이 하려던 얘기의 반대편이에요. 저는 "코드에서 뽑았으니 정확하다"고 생각했는데, 뽑은 범위가 데이터베이스 테이블까지였습니다. 브라우저에 남기는 건 테이블이 아니라서 시야에 안 들어왔어요. 표의 칸 이름이 "보관처"였던 것도 한몫했고요. 그 단어를 쓰는 순간 머리가 서버 쪽만 훑습니다.
지금 고칠 목록에 올려뒀습니다. 다음 배포 때 6절 약속대로 문서부터 고칠 거예요. 아마 이렇게 한 줄 늘어나겠죠 — "중복 집계 방지 / 마지막 조회 시각, 방문 표시 / 읽는 분 브라우저 / 브라우저를 지우면 함께 지워짐."
덤: 이 표가 폰에서는 화면 밖으로 밀려 있었습니다
화면을 좁혀서 보다가 하나 더 나왔어요.

폰 크기(가로 390)로 방침 페이지를 열면 표가 오른쪽으로 잘립니다. "보관 기간" 칸이 화면 밖이에요. 표만 옆으로 미는 게 아니라 페이지 전체가 옆으로 밀립니다. 재보니 화면은 390인데 페이지는 552더라고요.
표 CSS에는 이런 주석이 달려 있습니다.
/* 개인정보처리방침의 수집 항목 표 — 좁은 화면에서는 가로로 밀어서 본다 */
의도는 맞았는데 실제로는 그렇게 안 되고 있었어요. 같은 사이트의 본문 표는 멀쩡합니다. 표 6개가 들어간 글을 같은 폰 크기로 열어봤더니 페이지 폭이 정확히 390이었어요. 본문 표는 표를 감싸는 상자를 하나 더 두고 그 상자에 스크롤을 걸어놨고, 방침 표는 표 자신한테 걸어놨거든요. 차이가 거기서 납니다.
이것도 배포는 성공했고, 오류도 안 났고, 데스크톱에서는 완벽하게 보였어요. 화면을 좁혀서 열어봐야만 보이는 종류의 고장입니다. 이 연재에서 몇 번째로 같은 말을 하는지 모르겠네요.
사람은 어디에 남았나
이 편에서 사람이 붙잡아야 했던 자리가 셋이었습니다.
첫째, 두 페이지를 나란히 놓고 읽은 자리. 푸터의 "데이터베이스를 쓰지 않습니다"와 방침의 "Supabase에 저장합니다"는 각각 놓고 보면 둘 다 멀쩡한 문장이에요. 문법도 맞고 오류도 안 납니다. 코드도 아무 말 안 해줘요. 두 문장이 서로 어긋난다는 건 한 사람이 둘 다 읽어야만 보입니다. 저는 방침을 쓰다가 겨우 봤어요. 38분 만에 거짓이 된 문장을 이틀 뒤에야 발견한 거니까 자랑할 일은 아니고요.
둘째, 뽑아낸 범위를 의심한 자리. "코드에서 뽑았다"는 말은 안심하기 딱 좋은 말입니다. 그런데 뽑은 게 데이터베이스 테이블뿐이었고, 브라우저에 남기는 두 줄은 빠졌어요. 자동으로 훑어서 나온 목록이 완전한지는 자동으로 알 수 없습니다. "이 목록에 안 들어올 만한 게 뭐가 있지?"를 묻는 건 사람 몫이었어요.
셋째, 화면을 좁혀본 자리. 주석에는 "좁은 화면에서는 가로로 밀어서 본다"고 적혀 있었는데 실제로는 페이지가 통째로 밀리고 있었습니다. 주석은 의도고, 화면은 결과예요. 둘이 다를 수 있다는 걸 확인하려면 열어보는 수밖에 없습니다.
그리고 이 편에서 제일 크게 배운 건 이거예요.
개인정보처리방침은 쓰는 문서가 아니라 확인하는 문서입니다. 양식을 채우는 일이라고 생각하면 30분이면 끝나요. 근데 그렇게 만든 문서는 제 사이트에 대해 아무것도 말해주지 않고, 틀리기까지 합니다. 반대로 "지금 내 코드가 실제로 뭘 저장하지?"에서 시작하면, 문서 한 장을 쓰는 동안 사이트에 있던 거짓말 하나를 찾게 되고, 표에 빈 칸이 하나 있다는 것도 알게 돼요.
광고를 달려고 시작한 일이었는데, 결과적으로는 제 사이트를 처음으로 제대로 읽은 셈이 됐습니다. 그게 제일 남는 장사였어요.
다음 편은 ads.txt와 동의 메시지 얘기입니다. 승인이 난 다음에 해야 하는 것들이요.
#개인정보처리방침 #개인정보처리방침작성법 #프라이버시정책 #애드센스 #구글애드센스 #애드센스승인조건 #쿠팡파트너스 #제휴마케팅 #블로그수익화 #광고쿠키 #GDPR #동의관리 #쿠키정책 #웹사이트만들기 #개인홈페이지 #정적사이트 #CloudflarePages #슈파베이스 #Supabase #익명댓글 #조회수카운터 #로컬스토리지 #RLS #행수준보안 #Web3Forms #AI코딩 #클로드 #바이브코딩 #휴먼인더루프 #빅터하우스