사이트와 댓글을 관리하는 어드민, 개인 정보는 최소로
백엔드는 API로 두고, 관리 화면은 따로
2026/09/29 17:10사이트와 댓글을 관리하는 어드민, 개인 정보는 최소로
4편까지 댓글을 받고 보여 주는 쪽을 다뤘다. 이번에는 그 반대편, 사이트를 등록하고 달린 댓글을 관리하는 어드민을 다룬다. 로그인과 토큰을 어떻게 다루는지, 어떤 화면이 있는지 차례로 적고, 마지막 에이전틱 코딩 노트에는 코딩 에이전트인 Claude Code와 여러 저장소를 어떻게 나눠 작업했는지 적는다.
어드민을 따로 둔 이유

백엔드는 순수한 API 서버로 두고 싶었다. 관리 화면까지 백엔드에 넣으면 API 서버가 HTML을 그리고 세션을 다루는 일까지 떠안게 된다. 그래서 어드민은 Next.js로 따로 만들어 Vercel에 올렸고, 백엔드의 관리자용 API(/admin/...)를 부르는 클라이언트 중 하나로 두었다.
어드민이 백엔드를 부르는 코드는 모두 서버에서 돈다. 화면에서 버튼을 누르면 Next.js의 Server Action이 서버에서 백엔드를 호출하고, 결과만 화면으로 돌려준다.
구글 로그인
로그인은 구글 계정으로만 받는다. 개인 식별 정보를 최대한 받고 싶지 않았다. 아이디와 비밀번호를 직접 받으면 그걸 안전하게 보관하는 책임까지 져야 한다. 구글 로그인을 쓰면 비밀번호는 아예 다루지 않고, 백엔드에는 구글 계정 ID와 이메일, 이름, 프로필 사진 주소만 남는다.
로그인 흐름은 이렇다. 어드민이 구글에서 받은 ID 토큰을 백엔드에 넘기면, 백엔드가 구글에 그 토큰이 진짜인지 확인한다. 확인이 끝나면 처음 온 사용자는 새로 만들고, orbithall 자체의 토큰을 발급한다. 이후 어드민은 이 토큰으로 백엔드를 부른다.
토큰은 서버에만 둔다
어드민은 Next.js의 인증 라이브러리인 Auth.js를 쓴다. Auth.js는 세션을 암호화된 쿠키에 담기 때문에, 그 안에 넣은 백엔드 토큰은 브라우저의 스크립트가 읽을 수 없다. 다만 브라우저에 내려가는 세션 정보에 무엇을 실을지는 직접 정해야 한다. 백엔드 토큰은 세션 정보에 싣지 않고, 서버에서만 꺼내 쓰도록 정했다. 서버 로그에 요청과 응답을 남길 때도 토큰과 작성자 IP는 가려서 남긴다.
토큰은 흔히 쓰는 구성대로 둘로 나눴다. 백엔드를 부를 때 쓰는 Access Token과, Access Token이 만료되면 새로 받아 오는 데 쓰는 Refresh Token이다.
| 토큰 | 수명 | 비고 |
|---|---|---|
| Access Token | 7일 | 만료 1분 전에 어드민이 미리 갱신 |
| Refresh Token | 14일 동안 안 쓰면 만료, 최대 30일 | 한 번 쓰면 새 토큰으로 교체 |
Access Token의 수명은 7일로 길게 잡았다. 어드민은 정해진 환경에서 쓰고, 여러 계정을 자주 바꿔 가며 쓸 일도 거의 없으니 길게 잡아도 괜찮다고 봤다. 수명은 설정값이라, 실제로 써 보면서 필요하면 언제든 줄일 수 있다.
Refresh Token도 널리 쓰이는 회전 방식(Refresh Token Rotation)을 따른다. 한 번 쓰면 새 토큰으로 바뀐다. 이미 쓴 토큰이 다시 들어오면 누군가 토큰을 훔쳐 쓴 것으로 보고, 그 토큰에서 이어진 토큰을 모두 폐기한다. 여러 요청이 거의 동시에 같은 토큰으로 갱신하는 경우를 위해 30초의 여유는 둔다. 백엔드는 Refresh Token을 원문 대신 해시로만 저장한다.
사이트 관리

사이트를 등록할 때는 이름과 도메인, 댓글을 받을 도메인 목록을 넣는다. 등록하면 그 사이트 전용 API 키가 발급되고, 위젯은 이 키로 댓글을 주고받는다. 사이트 상세 화면에서는 포스트 수와 댓글 수, 삭제된 댓글 수 같은 통계와 포스트 목록을 볼 수 있다.
사이트는 등록한 사람만 조회하고 고칠 수 있다. 다른 사람의 사이트를 조회하면 존재하지 않는 것처럼 응답한다. 다만 지금은 도메인의 실제 주인인지 확인하는 단계가 없어서, 같은 도메인을 먼저 등록한 사람이 차지할 수 있다. 이건 회고 편에서 숙제로 다루겠다.
댓글 관리

포스트를 누르면 그 포스트의 댓글을 모두 볼 수 있다. 작성자가 지운 댓글도 "삭제됨" 표시와 함께 보이고, 관리자는 스팸이나 부적절한 댓글을 직접 지울 수 있다. 관리자가 지워도 답글은 남는다. 3편에서 정한 대로, 지운 쪽의 의지와 답글을 단 쪽의 의지를 똑같이 존중한다.
작성자 IP는 관리자 화면에서도 기본으로 가려져 있고, 눌러야 원본이 보인다. 어렵지 않은 작업이라 보안을 위해 한 겹 더 두었다. 개인 식별 정보를 최대한 덜 드러내고 싶다는 생각이 여기까지 이어진 셈이다.
에이전틱 코딩 노트
어드민은 백엔드와 다른 저장소다. 두 저장소를 오가는 작업은 간단하거나, 한쪽에 완전히 딸린 작업이거나, 다른 쪽의 선행 조건일 때만 한 세션에서 한다. 그런 경우는 아주 드물고, 거의 모든 작업은 저장소마다 별도의 에이전트와 별도의 메모리로 진행했다.
좋은 코드에서 캡슐화와 응집도, 책임과 한계가 중요하듯이, 프로젝트 사이에서도 그렇다고 생각한다. 특히 에이전트와 일할 때는 맥락이 섞이면 잘못된 판단 하나가 오염시키는 범위가 커진다. 저장소마다 에이전트를 나누면, 한쪽의 판단을 다른 쪽이 교차 검증할 수 있고, 두 저장소 사이의 약속도 정리된 인터페이스로 남는다.
토큰 갱신이 그런 예다. 백엔드 쪽 에이전트가 갱신 방식을 명세 문서로 정리해 API를 배포하고, 어드민 쪽 에이전트는 그 명세와 배포된 API 문서만 보고 연동했다. 두 에이전트는 서로의 대화를 모른 채, 문서와 API로만 이어졌다. 어드민 쪽에서 명세를 검토하며 나온 의견(에러 응답 형식 통일, 동시 갱신 때의 응답 방식)은 백엔드 명세에 반영됐다.
다음 편에서는 이 서비스를 무료 인프라로 운영하고, DB를 옮긴 이야기를 다룬다.
저장소
- 어드민: june20516/orbithall-admin
- API 서버: june20516/orbithall