Victor

[연재] 정적 사이트에 댓글 붙이기 — Supabase

연재 · 2026-08-21

등록을 눌렀는데, 아무 일도 안 일어났습니다

 

댓글 칸을 만들어서 올린 날 밤이었어요. 배포까지 다 끝내고 나서, 글 하나를 열고 맨 아래로 내려가서 등록을 눌러봤습니다.

 

아무 일도 안 일어났어요.

 

폼이 반응을 안 했습니다. 성공했다는 말도, 뭐가 틀렸다는 빨간 글씨도 없이 그냥 조용했어요.

 

화면은 멀쩡했어요. 그게 제일 고약했습니다. 댓글 칸은 예쁘게 그려져 있고, 글씨도 잘 써지고, 안내 문구도 제자리에 있고요. 눈으로 보면 아무 문제가 없는데 눌러야만 알 수 있는 고장이었어요.

 

원인은 허무했습니다. 화면에 심어둔 자바스크립트 조각 안에서, 문자열 하나가 중간에 줄바꿈으로 끊겨 있었어요. 그러면 브라우저는 그 조각 전체를 문법 오류로 보고 통째로 안 돌립니다. 댓글 등록도, 목록 불러오기도, 전부 한꺼번에 죽은 거죠. 그런데 HTML은 멀쩡하니까 화면은 그대로 그려집니다.

 

더 나쁜 건, 제가 이걸 배포한 다음에야 알았다는 겁니다.

 

그래서 그날 커밋에 이런 걸 하나 같이 넣었어요.

 

def check_scripts() -> None:
    """생성된 인라인 <script> 의 문법을 검사한다.

    문자열 안에 줄바꿈이 섞이는 식으로 스크립트가 통째로 죽어도
    화면은 멀쩡해 보인다(댓글 폼이 그냥 반응만 안 한다). 배포 전에 잡는다.
    """

 

만들어진 페이지들을 전부 훑어서 그 안의 스크립트 조각을 하나씩 꺼내고, node --check 로 문법만 확인합니다. 하나라도 깨져 있으면 배포를 중단시켜요. 실행해보는 게 아니라 문법만 보는 거라 순식간에 끝납니다.

 

고치고 나서 지금은 이렇게 생긴 칸입니다. 글 맨 아래에 붙어 있어요.

 

 

오늘 이야기는 여기서 시작해서, 마지막엔 좀 다른 데로 갑니다. 댓글을 붙였더니 제 사이트가 이틀 동안 거짓말을 하고 있었다는 걸 알게 됐거든요. 코드는 그 거짓말에 대해 아무 말도 안 해줬습니다.

 

서버가 없으면, 남이 쓴 글은 어디에 두나요

 

이 연재 4편에서 저는 서버를 안 쓰기로 정했어요. 글을 미리 HTML 파일로 다 구워놓고, 그 파일들을 그냥 올려두는 방식입니다. 돌아가는 프로그램이 없으니 터질 것도 없고, 공짜고, 빠릅니다.

 

그 선택이 처음으로 벽에 부딪힌 게 댓글이었어요.

 

글은 제가 미리 만들어두면 되잖아요. 그런데 댓글은 제가 미리 만들 수가 없습니다. 누가 언제 뭘 쓸지 모르니까요. 누군가 등록을 누르는 순간, 그 글자를 어딘가에 저장해야 하고, 다음 사람이 들어왔을 때 그걸 꺼내 와야 합니다. 저장하고 꺼내오는 일은 서버가 하는 일이고요.

 

여기서 길이 두 갈래로 갈립니다.

 

하나는 서버를 두는 거예요. 정적 사이트라는 선택을 취소하는 겁니다. 돈이 들고, 관리할 게 생기고, 새벽에 터지면 제가 일어나야 해요.

 

다른 하나는 남의 서버를 빌리는 겁니다. 화면은 그대로 정적 파일로 두고, 저장이 필요한 것만 밖에 맡기는 거죠. 저는 이쪽을 골랐어요. Supabase 라는 서비스를 씁니다.

 

이게 이 편의 반전이에요. "서버가 없다"가 "아무것도 저장 안 한다"는 뜻은 아니었습니다. 저는 그 둘을 같은 말로 알고 있었어요.

 

구조는 이렇게 돼요.

 

어디 있나누가 만드나
글·화면·이미지Cloudflare Pages (정적 파일)제가 미리 구워서 올림
댓글Supabase방문자가 그때그때
조회수Supabase방문자가 그때그때

 

브라우저가 페이지를 받아온 다음, 브라우저가 직접 Supabase 에 "이 글 댓글 좀 주세요" 하고 물어봅니다. 제 서버를 거치지 않아요. 애초에 제 서버가 없으니까요.

 

 

이 첫 화면 자체는 미리 구워둔 파일 한 장이에요. 제목도, 소개 글도, 도구 카드도 전부 제가 만들어서 올려둔 글자입니다. 딱 하나, 네 번째 글 날짜 옆에 붙은 "조회 6" — 저것만 페이지가 뜬 뒤에 밖에서 받아온 값이에요. 오늘 이야기가 저 자리에 관한 겁니다.

 

여기서 한 가지 안 한 게 있어요. Supabase 는 편하게 쓰라고 전용 도구 꾸러미(SDK)를 주는데, 그게 60KB쯤 됩니다. 저는 그걸 안 얹고 브라우저에 기본으로 들어 있는 기능만 써서 직접 주소를 두드렸어요. 제가 쓰는 기능이 "읽기"랑 "쓰기" 둘뿐이라, 60KB를 매 페이지마다 내려받게 할 이유가 없더라고요.

 

그리고 비용이요. 한 푼도 안 늘었습니다. 카드 혜택 도구에서 이미 쓰고 있던 Supabase 프로젝트에 테이블만 하나 더 만들었거든요.

 

로그인을 안 붙였습니다

 

이게 이 편에서 제가 한 첫 번째 판단이에요.

 

댓글에 로그인을 붙이는 건 어렵지 않습니다. Supabase 는 회원가입·로그인 기능을 그냥 켜기만 하면 되게 해놨어요. 실제로 카드 혜택 도구에는 로그인을 붙였고요.

 

문제는 만들기가 쉽다고 쓰기도 쉬운 건 아니라는 것입니다.

 

생각해보세요. 개인 블로그 글 하나 읽다가 "잘 봤습니다" 한 줄 남기려고 이메일 넣고 비밀번호 정하고 인증 메일 확인하고 다시 돌아오시겠어요? 저라면 그냥 창을 닫습니다.

 

그래서 로그인 없이 쓰게 했어요. 이름 칸은 있는데 비워도 됩니다. 비우면 "익명"으로 붙어요.

 

 

화면에 적힌 문구도 그렇게 뒀습니다.

 

댓글을 남겨주세요. 로그인 없이 쓸 수 있습니다.

 

이름 (비우면 익명)

 

대신 대가가 있어요. 누가 썼는지 모르니까, 본인이 자기 댓글을 지울 수도 없습니다. 로그인을 안 했으니 "이게 당신 댓글이 맞다"를 증명할 방법이 없잖아요. 그래서 그 문장을 폼 밑에 그대로 적어뒀습니다.

 

로그인 없이 쓰는 대신, 등록한 댓글은 직접 지울 수 없습니다. 지워야 할 댓글은 옆의 신고를 눌러주세요.

 

이걸 안 적고 넘어갈 수도 있었어요. 아무도 안 물어볼 테니까요. 그런데 그건 나중에 누가 자기 댓글을 지우려다 못 지웠을 때 훨씬 기분 나쁜 일이 됩니다. 불편한 조건은 겪기 전에 알려주는 게 낫더라고요.

 

지울 권한을 아무에게도 안 줬습니다

 

로그인이 없다는 건, 다르게 말하면 모두가 같은 열쇠 하나를 들고 있다는 뜻이에요.

 

댓글을 읽고 쓰려면 브라우저가 Supabase 에 열쇠를 보여줘야 하는데, 이 열쇠는 페이지 안에 그냥 적혀 있습니다. 누구나 볼 수 있어요. 공개용으로 나온 열쇠라서 그래도 됩니다.

 

그럼 그 열쇠로 남의 댓글을 지울 수 있는 거 아니냐고요? 여기가 핵심인데, 못 지웁니다.

 

Supabase 쪽 테이블에 권한 규칙을 이렇게 걸어놨거든요.

 

-- 읽기는 누구나, 쓰기도 누구나(익명 허용). 수정·삭제 정책은 두지 않는다
-- → anon 키로는 남의 댓글을 고치거나 지울 수 없다.
create policy "read all"       on public.comments for select using (true);
create policy "anyone can write" on public.comments for insert with check (true);

 

읽기 규칙을 만들었고, 쓰기 규칙을 만들었어요. 수정 규칙과 삭제 규칙은 아예 안 만들었습니다.

 

이 데이터베이스는 "규칙이 없으면 금지"로 동작해요. 그러니까 "삭제는 금지한다"고 적을 필요가 없습니다. 적지 않는 게 곧 금지예요. 잠그는 걸 깜빡할 위험 자체가 없는 구조라서, 저는 이 방식이 마음에 들었습니다.

 

지워야 할 댓글이 생기면요? 댓글마다 옆에 신고 버튼이 붙어 있습니다. 누르면 사이트 우상단에 원래 있던 "신고·제안" 창이 열리고, 어느 글의 몇 번 댓글인지가 미리 채워진 채로 뜹니다. 저는 그 연락을 받아서 관리자 화면에서 직접 지워요.

 

 

신고를 누른 사람이 "무슨 글의 무슨 댓글"인지 다시 적게 만들면, 대부분은 귀찮아서 안 보냅니다. 그래서 사람이 적어야 할 건 사유 한 줄만 남겼어요.

 

스팸은 네 겹으로 막았습니다

 

로그인을 없앴다는 건 봇한테도 문을 열어줬다는 뜻이에요. 그래서 봇이 흔히 하는 짓만 골라서 네 군데에 그물을 쳤습니다.

 

1. 사람 눈에 안 보이는 칸 하나 — 폼 안에 빈 칸을 하나 더 넣어두고, 화면 밖으로 밀어놨어요. 사람은 있는지도 모르니까 안 채웁니다. 봇은 폼에 있는 칸을 전부 채우는 습성이 있어서 여기도 채워요. 채워져 있으면 그냥 무시합니다.

 

2. 30초 안에 또 못 쓰게 — 한 번 등록하면 30초 동안은 등록 버튼이 안 먹습니다.

 

3. 같은 내용을 10분 안에 또 못 쓰게 — 이건 브라우저가 아니라 데이터베이스 쪽에서 막아요. 똑같은 본문이 10분 안에 또 들어오면 거절합니다. 봇의 제일 흔한 패턴이 같은 문구를 여기저기 뿌리는 거거든요. 사람이 같은 말을 10분 안에 두 번 쓰는 일은 드뭅니다.

 

4. 링크가 3개 넘으면 거절 — 이것도 데이터베이스 쪽입니다.

 

2번은 브라우저에서 막고, 3·4번은 데이터베이스에서 막습니다. 이 구분이 중요해요. 브라우저에서 막는 건 마음먹으면 뚫립니다. 개발자 도구를 열어서 조건을 지워버리면 그만이거든요. 그래서 진짜 막아야 하는 건 저쪽 끝, 데이터베이스에 걸어야 합니다.

 

실제로 되는지 제가 직접 해봤어요. 먼저 한 글자만 쓰고 등록을 눌러봤습니다.

 

 

빨간 글씨로 "2자 이상 적어주세요."가 떴어요. 이건 브라우저가 막은 겁니다. Supabase 까지 가지도 않았어요.

 

그다음엔 링크를 다섯 개 넣어봤습니다.

 

 

같은 내용이 방금 등록되었거나 링크가 너무 많습니다.

 

이건 요청이 Supabase 까지 갔다가 거절당해서 돌아온 거예요. 저장은 안 됐습니다. 테이블을 다시 확인해보니 빈 상태 그대로였어요.

 

메시지 문구를 저렇게 두 개를 합쳐놓은 데는 이유가 있습니다. 데이터베이스가 거절할 땐 "왜" 거절했는지를 자세히 알려주는데, 그걸 그대로 화면에 보여주면 어떤 규칙이 걸려 있는지가 다 드러나요. 스팸 보내는 쪽에 규칙표를 나눠주는 셈이죠. 그래서 뭉뚱그렸습니다.

 

300편짜리 목록에서, 요청을 두 번으로

 

댓글을 붙이고 나니 욕심이 생겼어요. 글 목록에서도 어느 글에 댓글이 달렸는지 보이면 좋겠더라고요.

 

여기서 조심할 게 하나 있었습니다.

 

이 글을 쓰던 8월 16일 기준으로 사이트에 글이 310편 올라와 있었어요. 한 줄 한 줄이 "내 댓글 몇 개예요?" 하고 물어보면 페이지 하나 여는 데 요청이 310번 날아갑니다. 글이 늘어날수록 더 느려지고요. 지금 300편인데 400편, 500편이 되면요?

 

그래서 반대로 했어요. 한 번에 전부 받아서, 화면에서 나눕니다.

 

/* 목록의 조회수·댓글수를 한 번에 채운다. 글 수가 많아도 요청 두 번이면 된다. */
Promise.all([
  fetch(U + '/rest/v1/post_views?select=post_slug,views', ...),
  fetch(U + '/rest/v1/comments?select=post_slug', ...)
])

 

조회수 표를 통으로 한 번, 댓글이 어느 글에 달렸는지를 통으로 한 번. 요청 두 번이면 끝납니다. 글이 1,000편이 돼도 두 번이에요.

 

 

날짜 옆에 "조회 6" 처럼 붙은 게 그렇게 채워진 값이에요. 댓글이 달린 글에는 그 옆에 "댓글 N"이 같이 붙습니다.

 

숫자가 들어오고 나면 정렬 기준 하나가 더 쓸 수 있게 돼요. 조회수순입니다.

 

 

이건 순서 문제가 좀 까다로웠어요. 페이지가 뜨는 시점엔 숫자가 아직 안 왔거든요. 그래서 처음엔 최신순으로 그려두고, 숫자가 도착하면 "지금 조회수순으로 보고 계시면 다시 정렬"하게 해뒀습니다. 최신순으로 보고 계신 분 화면이 갑자기 흔들리면 안 되니까요.

 

글 하나를 열었을 때는 제목 바로 밑, 날짜 옆에 붙습니다.

 

 

조회수를 세는 이야기는 따로 할 게 좀 있어서 다음 편으로 미뤄둘게요. 여기선 댓글 수 옆에 나란히 붙는다까지만요.

 

폭이 좁은 화면에서는 이름 칸과 등록 버튼이 한 줄에 들어가게 해뒀습니다.

 

 

그리고, 제 사이트가 이틀째 거짓말을 하고 있었습니다

 

여기부터가 오늘 진짜 하고 싶은 이야기예요.

 

댓글을 붙인 게 7월 22일 밤 8시 31분입니다. 조회수·댓글 수를 목록에 붙인 게 같은 날 밤 9시 29분이고요. 사이트 자체는 그날 저녁 7시 53분에 처음 만들었어요. 하루 저녁에 다 벌어진 일입니다.

 

그때 사이트 맨 아래 푸터에는 이렇게 적혀 있었어요.

 

전부 정적 페이지로 만들었습니다. 서버도 데이터베이스도 쓰지 않습니다.

 

저녁 7시 53분에는 저 문장이 사실이었습니다. 정말로 파일만 올려둔 사이트였거든요. 그게 자랑스러워서 적어둔 거예요.

 

그런데 8시 31분에 댓글이 붙으면서, 저 문장은 틀린 문장이 됐습니다. 데이터베이스를 쓰기 시작했으니까요.

 

그날 저는 그걸 몰랐어요. 다음 날도 몰랐고요.

 

생각해보면 당연합니다. 저는 댓글 기능을 만들고 있었지, 푸터를 보고 있지 않았거든요. 푸터는 이미 다 만들어놓은 거니까 쳐다볼 이유가 없었어요.

 

그리고 코드는 아무 불평도 안 했습니다.

 

이게 무서운 부분이에요. 문법이 틀리면 빌드가 멈춥니다. 스크립트가 깨지면 node --check 가 배포를 막아요. 그런데 "화면에 적힌 문장이 코드가 하는 일과 다르다"는 건 어떤 검사에도 안 걸립니다. 컴퓨터가 보기엔 그냥 글자니까요. 저 문장을 "서버도 데이터베이스도 씁니다"로 바꿔놔도 빌드는 똑같이 성공합니다.

 

이틀 뒤인 7월 24일 오후, 다른 걸 하다가 걸렸어요. 광고를 붙이려면 개인정보처리방침이 있어야 해서 그 문서를 쓰고 있었거든요. 무엇을 받아서 어디에 두는지를 표로 정리하는데, 첫 줄이 이랬습니다.

 

 

기능받는 것보관처보관 기간
댓글이름(선택), 내용, 작성시각Supabase삭제 요청 시까지
조회수글별 조회 횟수 (개인 식별 안 함)Supabase계속

 

이걸 쓰다가 손이 멈췄어요. 한 페이지에서는 "Supabase 에 저장합니다"라고 하고, 같은 페이지 맨 밑에서는 "데이터베이스를 쓰지 않습니다"라고 하고 있었던 거예요. 스크롤 한 번 내리면 두 문장이 같이 보입니다.

 

그날 푸터를 고쳤습니다. 코드에는 이유를 이렇게 적어뒀어요.

 

# 예전 푸터에는 "서버도 데이터베이스도 쓰지 않습니다"라고 적혀 있었다.
# 댓글과 조회수를 붙이면서 그 말이 사실이 아니게 됐다(Supabase 에 저장된다).
# 개인정보처리방침에 저장한다고 써놓고 푸터에서 아니라고 하면 앞뒤가 맞지 않는다.

 

바뀐 문장은 이겁니다.

 

글과 화면은 정적 파일로 만듭니다. 댓글과 조회수만 외부 데이터베이스에 저장합니다 — 개인정보처리방침

 

 

한 줄짜리 수정이에요. 코드로 치면 아무것도 아닙니다. 그런데 저는 이게 이 편에서 제가 한 일 중에 제일 중요한 일이었다고 생각해요.

 

AI는 제가 시킨 걸 정확히 해줬습니다. 댓글 테이블을 만들고, 권한을 걸고, 스팸 그물을 네 겹 치고, 요청을 두 번으로 줄이는 것까지요. 제가 시킨 범위 안에서는 흠잡을 데가 없었어요.

 

그런데 "댓글을 붙였으니 38분 전에 써 둔 저 문장이 이제 거짓말이 됐다"는 건 아무도 안 알려줍니다. 그건 코드의 문제가 아니라 앞뒤가 맞느냐의 문제거든요. 그리고 앞뒤가 맞는지는 사이트 전체를 자기 것으로 알고 있는 사람만 볼 수 있어요. 어제 뭐라고 써놨는지 기억하는 사람만요.

 

기능을 붙이는 일은 앞으로 나아가는 일이잖아요. 그런데 앞으로 나아갈 때마다 뒤에 적어둔 말들이 하나씩 틀려집니다. 화면의 안내 문구, 소개 글, 약속. 이건 자동으로 안 따라와요.

 

기능 하나 붙일 때마다 "이거 붙이면서 거짓말이 된 문장이 어디 있나" 한 번 훑어보는 일 — 저는 이게 앞으로도 계속 사람 몫일 거라고 봅니다.

 

개인정보처리방침 쪽에도 같은 이야기를 한 번 더 적어뒀어요.

 

 

본인의 댓글이나 계정을 지우고 싶으시면 화면 우상단 신고·제안 버튼으로 알려주세요. 확인 후 지워드립니다. 댓글은 로그인 없이 쓰는 구조라 본인이 직접 지우는 기능은 없습니다 — 대신 요청을 받아 처리합니다.

 

"직접 지우는 기능은 없습니다"를 굳이 적은 것도 같은 이유예요. 없는 걸 없다고 적어두지 않으면, 있는 줄 알고 찾다가 화가 납니다.

 

지금까지 댓글은 0개입니다

 

솔직하게 마무리할게요.

 

이렇게 만들어놨는데, 이 글을 쓰는 지금까지 달린 댓글은 한 개도 없습니다. 스팸도 없어요. 그물을 네 겹이나 쳤는데 걸린 게 없습니다.

 

당연하죠. 도메인 붙인 지 얼마 안 됐고 검색 유입이 거의 없으니까요. 조회수도 글 하나에 한 자릿수입니다.

 

그래도 후회는 없어요. 세 가지 이유에서요.

 

첫째, 없으면 절대 안 달립니다. 댓글 칸이 없는 사이트에 댓글이 달릴 확률은 0이에요. 있어도 안 달릴 수는 있지만, 없으면 확실히 안 달립니다.

 

둘째, 정적 사이트의 한계선이 어디인지 알게 됐어요. 서버 없이도 저장이 필요한 기능을 붙일 수 있더라고요. 화면은 정적 파일 그대로 두고, 저장할 것만 밖에 맡기면 됩니다. 이걸 알고 나니 다음에 뭘 붙일지 생각하는 폭이 넓어졌어요.

 

셋째, 첫 댓글이 달렸을 때 준비가 돼 있는 게 낫습니다. 누가 글을 읽고 뭔가 말하고 싶어진 그 순간에 쓸 데가 없으면, 그 사람은 그냥 나가요. 다시 안 옵니다.

 

지금은 이렇게 뜹니다.

 

첫 댓글을 남겨보세요.

 

이 문구를 언제쯤 다른 걸로 바꾸게 될지는 모르겠어요. 그래도 그날이 오면, 적어도 지울 권한 문제로 허둥대지는 않을 겁니다.

 

다음 편에

 

오늘 조회수 이야기를 계속 옆으로 미뤘잖아요. 다음 편이 그 이야기입니다.

 

숫자를 세는 건 쉬워 보이는데 막상 해보면 결정할 게 꽤 많아요. 같은 사람이 새로고침을 열 번 하면 조회수가 열 번 올라가야 하나요? 그럼 자기 글을 자기가 열어보는 것도 세야 하나요? 그리고 브라우저가 직접 숫자를 올릴 수 있다면, 그 숫자를 마음대로 만들어버리는 건 어떻게 막나요?

 

정직한 숫자를 만드는 이야기입니다.

 

#정적사이트 #Supabase #수파베이스 #댓글기능 #익명댓글 #댓글시스템 #서버리스 #백엔드없이 #RLS #행수준보안 #스팸방지 #허니팟 #클라우드플레어페이지스 #개인홈페이지 #홈페이지만들기 #블로그댓글 #개인정보처리방침 #조회수카운터 #웹개발 #프론트엔드 #자바스크립트 #파이썬 #RESTAPI #무료호스팅 #바이브코딩 #AI코딩 #클로드 #휴먼인더루프 #비개발자개발 #빅터하우스

댓글

    로그인 없이 쓰는 대신, 등록한 댓글은 직접 지울 수 없습니다. 지워야 할 댓글은 옆의 신고를 눌러주세요.
    네이버 블로그글 원문 보러가기 →유튜브 쇼츠만드는 장면 20초로 보기 →
    관리자 Victor · 문의는 우상단 신고·제안으로 남겨주세요.
    글과 화면은 정적 파일로 만듭니다. 댓글과 조회수만 외부 데이터베이스에 저장합니다 — 개인정보처리방침

    오류 신고 · 제안

    관리자 Victor에게 전달됩니다