B
Codeverse
로그인 없는 댓글을 받기 위해 정한 규칙들

로그인 없는 댓글을 받기 위해 정한 규칙들

문턱은 낮추고, 최소한의 저지선은 남기고

2026/09/23

로그인 없는 댓글을 받기 위해 정한 규칙들

1편에서 orbithall의 댓글은 로그인 없이 이름과 비밀번호만으로 남긴다고 했다. 문턱을 낮춘 만큼, 댓글 API는 그 전제 위에서 무엇을 허락하고 무엇을 막을지 하나하나 정해야 했다. 이번 편은 그 규칙들을 다루고, 마지막 에이전틱 코딩 노트에는 코딩 에이전트인 Claude Code와 이런 코드를 어떻게 함께 만들었는지 적는다.

데이터 모델

orbithall 댓글 데이터 모델

구조는 단순하다. 사이트 하나에 포스트가 여러 개, 포스트 하나에 댓글이 여러 개 달린다. 포스트는 블로그 글 하나에 대응하는 데이터다. 사이트에는 위젯이 쓸 API 키와 댓글을 받을 도메인 목록이 있고, 포스트는 사이트 안에서 slug로 구분한다. 댓글은 부모 댓글을 가리키면 답글이 된다.

위젯이 쓰는 공개 API는 네 개뿐이다.

요청 하는 일
GET /api/posts/{slug}/comments 포스트의 댓글과 답글 조회
POST /api/posts/{slug}/comments 댓글이나 답글 작성
PUT /api/comments/{id} 댓글 수정(비밀번호 필요)
DELETE /api/comments/{id} 댓글 삭제(비밀번호 필요)

포스트 데이터는 필요할 때 생성

블로그에 글을 새로 올려도 orbithall에 따로 등록할 필요가 없다. 위젯은 글의 slug로 댓글을 요청하고, 그 글에 첫 댓글이 달리는 순간 포스트 데이터가 만들어진다. 블로그 관리자는 할 일이 없고, DB에는 댓글이 달린 포스트만 남는다. 댓글이 하나도 없는 포스트가 목록에 잡혀 봐야 제공할 기능이 없으니, 자원을 아끼는 쪽을 택했다.

익명이라는 전제에서 정한 규칙

수정과 삭제는 30분까지

댓글을 고치거나 지우려면 작성할 때 넣은 비밀번호가 필요하고, 그마저도 작성 후 30분 안에만 할 수 있다. 비밀번호는 bcrypt로 해시해서 저장한다.

30분은 한 글을 읽고 그 글에 대한 관심이 이어지는 시간을 생각해 정했다. 글쓴이가 익명인 상황에서 그보다 긴 시간은 큰 의미가 없다고 봤다.

답글은 한 단계까지

답글에는 다시 답글을 달 수 없다. 익명으로 오가는 대화에서 특정 댓글에 초점을 맞춘 대화는 시간으로 보나 문맥으로 보나 그리 길게 이어지지 않는다고 봤다.

화면 문제도 있었다. 답글이 깊어질수록 들여쓰기가 쌓여 댓글 본문이 좁아지고, 모바일에서는 특히 보기 싫어진다. 실제로 운영을 하다가 답글의 문맥을 더 모아야 하는 경우가 자주 나온다면, 그때 기능을 여는 편이 합리적이라고 생각한다.

지운 댓글의 자리

삭제된 댓글이 화면에 보이는 방식

삭제는 데이터를 실제로 지우지 않고 삭제 표시만 남긴다. 화면에서는 답글이 달린 댓글을 지우면 작성자와 내용을 비우고 "삭제된 댓글입니다"라는 자리만 남긴다. 답글이 없는 댓글은 목록에서 아예 사라진다.

지운 사람의 의지와 답글을 단 사람의 의지를 똑같이 존중하고 싶었다. 댓글을 지웠다고 남이 단 답글까지 맥락 없이 사라지면 안 되고, 반대로 답글이 달렸다는 이유로 지운 댓글이 계속 보여서도 안 된다.

저장하는 것과 보여 주는 것

댓글을 쓸 때 작성자의 IP와 브라우저 정보(User-Agent)도 함께 저장한다. 익명 댓글에서 조작이나 사기를 막고, 익명이라는 이유로 말이 거칠어지는 걸 막는 최소한의 저지선이라고 생각했다. 다만 공개 응답에는 IP의 앞부분만 내보낸다.

{
  "id": 42,
  "post_id": 7,
  "author_name": "지나가던 사람",
  "content": "잘 읽었습니다!",
  "is_deleted": false,
  "ip_address_masked": "123.45.***.***",
  "created_at": "2026-09-23T10:12:00Z",
  "updated_at": "2026-09-23T10:12:00Z"
}

IPv4는 앞 두 자리, IPv6는 앞 두 그룹만 남기고 가린다. 전체 IP는 사이트 관리자가 어드민에서만 볼 수 있다.

봇과 스팸 대응

익명 댓글에서 가장 먼저 걱정되는 건 사람보다 봇이다. 막는 장치는 세 가지를 두었다.

먼저 댓글 본문과 이름에서 HTML 태그를 저장하기 전에 모두 걷어낸다.

다음은 레이트리밋이다. 같은 IP에서는 댓글을 분당 10개까지만 쓸 수 있고, 한꺼번에 몰아 쓸 수 있는 건 5개까지다. 로봇이나 에이전트가 댓글을 쏟아내는 건 막되, 실제 사람들이 주고받는 대화는 방해하지 않는 선으로 잡았다.

마지막은 도메인 확인이다. 위젯의 API 키는 블로그 페이지의 스크립트에 들어 있어서 누구나 볼 수 있는 값이다. 그래서 키만으로는 요청을 믿지 않고, 요청이 사이트에 등록된 도메인에서 왔는지를 함께 확인한다. 브라우저는 다른 도메인으로 요청을 보낼 때 지금 페이지의 주소를 Origin 헤더에 자동으로 붙이고, 페이지의 스크립트는 이 값을 바꿀 수 없다.

요청을 보낸 곳 결과
이 블로그에 붙은 위젯 등록된 도메인이라 통과
다른 사이트에 이 블로그의 키를 붙여 넣은 위젯 브라우저가 그 사이트 주소를 붙이므로 차단
curl이나 스크립트처럼 브라우저 밖에서 보낸 요청 헤더를 빼거나 꾸밀 수 있어서 통과

도메인 확인은 다른 사이트가 내 키로 댓글창을 띄우는 것은 막지만, 키를 복사한 봇이 API를 직접 부르는 것까지 막지는 못한다. 이 한계는 인지한 채 기술 부채로 받아들였다. 스크립트 몇 줄로 어느 페이지에나 심는 위젯 방식을 지키면서 이걸 막을 다른 방법을 떠올리기 어려웠다. Disqus 같은 서비스도 공개 키와 도메인 제한을 쓰고, 봇은 스팸 필터나 CAPTCHA 같은 별도 장치로 막는다. orbithall은 지금 봇 대응을 레이트리밋에 기대고 있고, 더 고도화 된 대책은 회고 편에서 이야기해보려 한다.

에이전틱 코딩 노트

입력 검증, HTML 제거, 레이트리밋처럼 보안에 가까운 코드는 다른 코드보다 조심스럽게 다뤘다.

우선 직접 구현하기보다 검증된 라이브러리를 쓰는 데 신경 썼다. 비밀번호는 bcrypt, HTML 제거는 bluemonday, 레이트리밋은 Go 확장 라이브러리의 rate 패키지를 썼다. 에이전트는 그럴듯한 코드를 금방 짜 주지만, 실수가 곧 사고로 이어지는 영역에서는 새로 짠 코드보다 많은 사람이 오래 써 온 코드가 안전하다고 봤다.

그리고 아는 범위 안에서는 꼼꼼히 읽었고, 모르는 부분은 에이전트들에게 교차 검증과 반복 검증을 시켰다. 한 에이전트가 짠 코드를 다른 에이전트가 검토하게 하고, 같은 질문을 여러 번 던져 답이 흔들리지 않는지 봤다. 에이전트의 답을 그대로 받아들이기보다 일단 의심하고 근거를 묻는 태도를 유지하려 했다.

다음 편에서는 이 API를 블로그 화면에 그려 주는 위젯을 다뤄보려 한다.

저장소

Tagsorbithall댓글API보안Claude Code에이전틱 코딩바이브 코딩