댓글에 손댈 수 있는 세 가지 방법을 비교한 표. 숨기기는 권한이 필요 없지만 내 브라우저에만 적용되고, 내 댓글 삭제는 본인만 5분 안에 답글이 없을 때만 가능하고, 남의 댓글 삭제와 차단은 관리자만 할 수 있다.

·10분 읽기·업데이트:

5분이 지나면 아무도 그 댓글을 못 지웁니다


댓글을 붙이고 2주쯤 굴렸습니다. 그사이에 알게 된 게 하나 있습니다.

관리자로 로그인하는 화면이 없습니다.

비밀번호 칸이 있길래 로그인인 줄 알았습니다

설정에 ADMIN_PASSWD라는 게 있습니다. 이름만 보면 관리자 비밀번호입니다. 저는 이걸 넣어두면 어딘가에 로그인 화면이 있을 거라고 생각했고, 설치 문서에도 그렇게 적어놨습니다.

틀렸습니다. 설정 정의부를 열어보면 한 줄로 나옵니다.

AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD"
  description:"admin basic auth password"`

basic auth. 화면 로그인이 아니라 API 호출할 때 쓰는 인증입니다. 백업 내려받기나 복원 같은 명령을 터미널에서 때릴 때 쓰는 거지, 브라우저에서 댓글을 지우라고 있는 게 아닙니다.

한 술 더 떠서, 이걸 켜면 서버가 뜰 때 로그에 이런 줄을 남깁니다.

if a.AdminPasswd != "" {
    log.Printf("[WARN] admin basic auth enabled")
}

[WARN]입니다. 만든 사람이 “이거 평소에 켜두는 거 아니다”라고 표시해둔 셈입니다. 문서도 같은 얘기를 합니다.

진짜 관리자는 다른 데 있었습니다. OAuth로 한 번 로그인한 다음, 그때 받은 내 계정 ID를 ADMIN_SHARED_ID에 적어두는 것. 그러니까 관리자는 화면이 아니라 계정입니다. 로그인 창을 찾을 게 아니라 계정을 등록했어야 했습니다.

저는 그걸 안 해뒀습니다.

그래서 지금 누가 뭘 지울 수 있는지 세어봤습니다

기분으로 판단하지 말고 소스를 보기로 했습니다. 제가 돌리는 버전 그대로 받아서요.

git clone --depth 1 --branch v1.16.4 \
  https://github.com/umputun/remark42.git

댓글에 손댈 수 있는 길은 세 개였고, 셋의 성격이 전부 달랐습니다.

1. 숨기기 — 서버까지 가지 않습니다

메뉴에 “숨기기”가 있습니다. 로그인 없이도 눌립니다. 저는 처음에 이걸 권한 구멍인 줄 알았는데, 앞 글에서 열어보니 이게 전부였습니다.

hiddenUsers[user.id] = user;
setItem(LS_HIDDEN_USERS_KEY, JSON.stringify(hiddenUsers));

브라우저 저장소에만 적습니다. 서버로 아무 요청도 안 갑니다. 다른 사람 화면에는 그대로 보이고, 제가 휴대폰으로 들어가도 그대로 보입니다. 쓰는 사람 눈앞에서만 치우는 기능입니다.

버그가 아니라 설계입니다. 다만 이름이 “숨기기”라서 오해하기 딱 좋습니다.

2. 내 댓글 지우기 — 창이 좁습니다

자기가 쓴 댓글은 자기가 지울 수 있습니다. 조건이 둘 붙습니다.

// edit allowed in editDuration window only
if s.EditDuration > 0 &&
   time.Now().After(comment.Timestamp.Add(s.EditDuration)) {
    return fmt.Errorf("too late to edit %s", commentID)
}

// edit rejected on replayed threads
if s.HasReplies(comment) {
    return fmt.Errorf("parent comment with reply can't be edited")
}

시간 제한과 답글 제한입니다. 기본 수정 시간은 5분(EDIT_TIME)이고, 그 안이라도 누가 답글을 달아버리면 그때부터 못 지웁니다. 답글이 붙은 글을 지우면 대화가 끊기니까 막아둔 거죠.

3. 남의 댓글 지우기, 차단 — 관리자만

나머지는 전부 관리자 전용입니다. 라우터 설정 한 줄에 다 나옵니다.

rapi.Mount("/admin").Route(func(radmin *routegroup.Bundle) {
    radmin.Use(authMiddleware.Auth,
               authMiddleware.AdminOnly, matchSiteID)

AdminOnly. 이 아래에 댓글 삭제, 사용자 차단, 글 읽기전용 전환, 인증 배지, 댓글 고정이 전부 묶여 있습니다. 관리자가 없으면 이 문 전체가 잠겨 있는 겁니다.

정리하면 이렇게 됩니다

세 개를 나란히 놓으니까 상황이 분명해졌습니다.

구분숨기기내 댓글 삭제남의 댓글 삭제·차단
권한필요 없음본인관리자
적용 범위내 브라우저만모두에게모두에게
제한없음5분 안, 답글 없을 때없음
기기 바꾸면초기화됨——

제 서버는 지금 오른쪽 칸이 통째로 비어 있습니다. 그러니까 누가 뭘 쓰고 5분이 지나면, 그 댓글은 아무도 못 지웁니다. 본인도, 저도요.

머리로는 “관리자 기능 없어도 되겠지” 했는데, 이렇게 표로 적어놓으니 얘기가 달라 보였습니다.

“알아서 걸러진다”는 장치가 하나 있는데

그래도 완전히 무방비는 아닙니다. 점수 장치가 있습니다. 댓글에 좋아요/싫어요를 누를 수 있고, 점수가 내려가면 흐려지고 더 내려가면 접힙니다.

LowScore      int `env:"LOW_SCORE"      default:"-5"`
CriticalScore int `env:"CRITICAL_SCORE" default:"-10"`

읽는 사람들이 알아서 걸러주는 구조입니다. 관리자가 없어도 도는 안전장치라 반가웠는데, 확인해보니 제 설정에서는 안 돕니다.

if !s.anonVote && strings.HasPrefix(user.ID, "anonymous_") {
    http.Error(w, "Access denied", http.StatusForbidden)
    return
}

익명 사용자가 투표를 누르면 403입니다. 그리고 그 anonVote가 켜지는 조건이 이렇습니다.

AnonVote: s.AnonymousVote && s.RestrictVoteIP,

설정 두 개를 다 켜야 켜집니다. 저는 둘 다 안 켰습니다.

로그인 없는 댓글을 켠 대가를 보여주는 그림. 익명 허용을 켜면 방문자가 익명 사용자가 되고, 익명 사용자는 투표가 403으로 막히며, 투표가 없으니 점수가 안 움직이고, 결국 점수 기반으로 흐리게 처리하거나 접는 장치가 한 번도 발동하지 않는다. 아래쪽에는 그 빈자리에서 사람 손 없이 실제로 도는 세 가지 — 이름 잠그기, 단어 거르기, 진짜 IP 알려주기 — 를 적어뒀다.

여기가 핵심입니다. 저는 “한마디 남기려고 로그인까지 하긴 싫다”는 이유로 익명 댓글을 켰습니다. 그 결정의 대가가 여기 있었습니다. 로그인하는 사람이 없으니 투표하는 사람도 없고, 투표가 없으니 점수가 안 움직이고, 점수가 안 움직이니 그 장치는 한 번도 발동하지 않습니다.

기능을 안 켠 게 아니라, 다른 결정 하나가 이 기능을 꺼버린 겁니다. 설정 화면만 봐서는 이게 안 보입니다. 둘 다 “켜져 있음”으로 보이거든요.

사람 없이 도는 것들

그럼 지금 실제로 뭐가 막아주고 있나. 켜둔 것 중에 사람 손이 안 가는 것만 추리면 이렇습니다.

이름 잠그기 — 익명으로 제 이름을 못 쓰게 막아뒀습니다. 사칭이 제일 곤란한 종류라서요.

RESTRICTED_NAMES=RyuMa,류마,관리자,admin,운영자

단어 거르기 — 특정 단어가 들어가면 저장 자체가 거부됩니다. 가리는 게 아니라 아예 안 받습니다. 대출·도박 광고가 대부분 여기서 걸립니다.

길이 제한 — 기본이 2048자인데 3000자로 올려뒀습니다. 긴 글을 쓰고 싶은 사람도 있을 테니까요. 도배 방지 목적이면 오히려 기본값이 낫습니다.

진짜 IP 알려주기 — 이게 은근히 중요합니다.

TRUSTED_PROXY=172.16.0.0/12

댓글 서버가 역방향 프록시 뒤에 있으면, 서버는 프록시가 헤더에 적어 넘겨주는 방문자 IP를 봅니다. 문제는 이걸 안 적어두면 그 헤더를 누가 보냈든 믿는다는 겁니다. 헤더를 직접 꾸며 보내면 IP를 마음대로 바꿀 수 있고, 도배 차단과 투표 중복 확인이 IP를 기준으로 하니 그대로 빠져나갑니다. 적어두면 이 대역(도커 내부망)에서 온 헤더만 믿습니다. 안 적으면 서버가 켜질 때 경고도 한 줄 찍습니다.

그래서 어떻게 하기로 했나

결론부터 적으면, 관리자 계정은 만들어두기로 했습니다.

지금 규모에서 관리자 기능이 당장 필요하냐고 하면 아닙니다. 댓글이 몇 개 안 됩니다. 문제는 그게 아니라, 필요한 순간이 예고 없이 온다는 쪽입니다.

지울 일이 생겼을 때 그 자리에서 OAuth 앱을 만들고 설정을 고치고 컨테이너를 다시 띄우려면, 그 몇십 분 동안 그 댓글이 그대로 떠 있습니다. 반대로 미리 만들어두면 평소엔 아무 일도 안 일어나고, 필요할 때 로그인만 하면 됩니다. 비용 차이가 큽니다.

주의할 게 하나 있습니다. CMS용으로 만들어둔 OAuth 앱은 같이 안 쓰는 게 낫습니다. 한 앱을 두 서비스가 쓰면 비밀값도 하나라서, 한쪽에서 새면 둘 다 갈아야 합니다. 새로 하나 더 만들었습니다. (예전에는 앱 하나에 콜백 주소를 하나만 넣을 수 있었는데, 2026년 8월부터 열 개까지 넣을 수 있게 바뀌었습니다.)

관리 기능을 볼 때 물어볼 것

이번 건으로 확인 순서 같은 게 생겼습니다.

이 버튼은 어디까지 가나 — 서버까지 가는지, 내 브라우저에서 끝나는지. “숨기기”처럼 이름만 봐서는 구분이 안 되는 게 많습니다. 개발자 도구 네트워크 탭을 열고 한 번 눌러보면 3초면 압니다. 요청이 안 나가면 그건 내 눈앞에서만 일어나는 일입니다.

되돌릴 수 있는 창이 몇 분인가 — 5분 같은 숫자가 기본값으로 박혀 있는 경우가 많습니다. 이게 지나면 본인도 못 건드립니다.

권한 없이 되는 일의 경계가 어디인가 — 라우터 설정을 보면 대개 한눈에 나옵니다. AdminOnly 같은 단어로 묶여 있는 목록이 곧 “관리자가 없으면 못 하는 일” 목록입니다.

내가 켠 다른 설정이 이걸 끄고 있진 않나 — 이번에 제일 크게 걸린 게 이겁니다. 설정 화면에서는 둘 다 켜져 있는 것처럼 보이는데, 실제로는 한쪽이 다른 쪽을 막고 있었습니다. 설정은 따로 있어도 동작은 같이 돕니다.

마지막으로

관리자 기능을 안 만들고 운영하는 게 가능하냐고 물으면, 지금 규모에서는 가능합니다. 실제로 2주 동안 아무 일도 없었습니다.

다만 “가능하다”와 “괜찮다”는 다릅니다. 괜찮은 이유가 아직 아무 일도 안 일어나서라면, 그건 대비가 아니라 운입니다.

ADMIN_SHARED_ID 한 줄이면 됩니다. 5분짜리 일을 안 해두고 있었습니다.

고친 곳

2026년 9월 29일, 글에 붙일 참고 문서를 하나씩 다시 열어보다가 본문과 어긋나는 곳을 찾아 고쳤습니다.

  • TRUSTED_PROXY 설명을 고쳤습니다. 처음엔 안 적으면 모든 방문자가 같은 IP로 보인다고 적었는데 반대였습니다. 안 적으면 누가 보낸 IP 헤더든 믿어서, IP를 위조해 도배 차단을 피할 수 있습니다.
  • OAuth 앱을 따로 만드는 이유를 고쳤습니다. GitHub OAuth 앱은 2026년 8월부터 콜백 주소를 열 개까지 넣을 수 있습니다.

참고한 문서


댓글

로그인도 이메일도 필요 없습니다. 이름만 적고 편하게 남겨주세요.