
화면을 고쳐 나가는 모습을 그린 삽화예요.
AI 에이전트에게 최대한 자율성을 주면 어느 정도의 결과물이 나올지 궁금했어요. 필요한 기능과 아키텍처 정도만 정해 두고, 나머지는 에이전트가 스스로 판단해서 구현하도록 맡겨 봤어요.
사진 커뮤니티와 Game Mate, 개인 블로그를 그렇게 만들기 시작했어요. 화면 구성이나 세부 동작을 하나씩 지시하기보다 에이전트가 정하도록 두고, 결과물이 나오면 직접 써 보고 코드를 읽었어요. 마음에 들지 않거나 빠진 부분이 있으면 어떤 동작을 원하는지 다시 설명하고 수정을 맡겼어요.
실행해 보니 요청한 기능이 있어도 제가 쓰고 싶은 모습과는 차이가 있었어요. 사진을 보러 왔는데 그룹 목록이 먼저 나오거나, 사진 위에 긴 안내가 붙는 식이었어요. 코드에서도 역할이 섞이거나 같은 상태를 여러 곳에서 다루는 부분을 다시 봐야 했어요.
제가 처음에 정하지 않은 정책도 많았어요. 익명으로 쓴 글은 로그아웃 뒤에 어떻게 찾을지, 접수 중에 열어 둔 화면에서 마감 후 제출하면 어떻게 할지까지 적어 둔 건 아니었어요. 나온 화면과 코드를 검토하면서 이런 경우를 하나씩 정했고, 그에 맞게 구현도 다시 고쳤어요.
처음 만들기 시작한 건 photo-contest였어요. 한 방송인의 '영도 대회'라는 콘텐츠에서 아이디어를 얻었어요. 친한 사람끼리 그룹 안에서 재밌는 사진을 공유할 수 있는 서비스가 있으면 좋겠다고 생각했어요.
그룹에 친구를 초대하고 사진을 올린 뒤 투표하는 형태로 시작했어요. 제출 기간과 투표 기간을 나누고 공동 우승이나 득표가 없는 경우도 처리했어요.

테스트 계정으로 실행한 Game Mate 피드와 별사대의 그룹 탐색 화면이에요.
사진을 보러 왔는데 그룹 목록부터 나왔어요
처음 띄운 홈은 그룹을 고르는 화면에 가까웠어요. 제가 원한 건 가입한 그룹의 사진이 최신순으로 보이는 화면이었어요. 그룹 목록은 마이페이지로 옮기고, 홈에서는 바로 사진을 보게 했어요.
홈을 사진 중심으로 바꾸고 나니 대회가 끝나면 더 올릴 수 없는 것도 아쉬웠어요. 기간을 정하지 않고 계속 게시할 수 있게 하고, 동영상과 합성 이미지도 받도록 넓혔어요. 관리자의 게시물 삭제와 이달의 인기 게시물도 여기에 더했어요.
디자인도 실행해 보니 고칠 게 있었어요. 사진을 가리는 배치는 바꾸고, 스토리 기능이 없는데 붙어 있던 테두리는 지웠어요. 데스크톱에서 봤을 때 여백과 배치도 따로 확인했어요.
그룹 가입도 버튼을 누르면 가입되는 것만 확인해서는 부족했어요. 공개 그룹의 대회 화면에서 가입한 직후에는 글쓰기 버튼이 나타나지 않고, 페이지를 나갔다가 다시 들어와야 보이는 문제가 있었어요. 가입 정보가 바뀌었을 때 대회에 게시할 수 있는지도 바로 갱신하도록 고쳤어요. 기능이 있는지 확인하는 것과 그 화면에서 자연스럽게 이어서 쓸 수 있는지 확인하는 건 달랐어요.
시작 화면에는 로그인, 회원가입, '익명으로 시작하기'를 모았어요. 익명으로 들어와 공개 그룹을 둘러볼 수 있게 했고, 비공개 그룹은 로그인과 가입 승인을 거치게 했어요.
여기서 익명은 단순히 로그인 화면을 건너뛰는 기능은 아니었어요. 외부 계정을 연결하지 않고 쓸 수 있게 하려고 소셜 로그인도 넣지 않았어요.
익명으로 시작한 뒤의 기록도 생각해야 했어요
익명으로 들어오는 길을 만들고 나니 그다음이 걸렸어요. 글을 쓰고 그룹에 가입한 사람이 다른 기기에서 계속 쓰거나, 로그아웃 뒤 다시 돌아오려면 기존 프로필을 찾을 방법이 필요했어요.
익명 상태에서 회원가입하면 쓰던 프로필을 그대로 계정으로 바꾸고, 가입한 공개 그룹과 작성한 글을 이어가게 했어요. 회원가입이 기존 활동과 떨어진 새 계정을 만드는 일이 되면, 기록을 유지하려고 가입한 사람에게는 곤란한 결과가 되니까요.
반대로 회원가입 전에 로그아웃하면 그 익명 프로필과 기록에 다시 접근할 수 없어요. 이때 '기록이 삭제된다'고 쓰는 것도 정확하지 않았어요. 다른 사람에게 보이는 글이 없어지는 것과, 작성자가 자기 프로필로 다시 들어올 수 없는 것은 다른 일이었어요.
그래서 마이페이지의 '브라우저 세션으로 유지돼요'라는 설명도 바꿨어요. 로그아웃을 누를 때 확인 창에서 기록에 다시 접근할 수 없다는 결과를 먼저 알리고, 계속 쓰려면 회원가입으로 이어지게 했어요. 세션이 무엇인지 몰라도 왜 가입이 필요한지 알 수 있도록 한 거예요.
초대받았다고 비공개 그룹에 바로 들어가면 안 됐어요
그룹은 처음에 초대 링크로 친구를 부르는 기능이었어요. 공개 그룹과 비공개 그룹을 나누고 나니, 링크를 아는 것과 글을 볼 권한이 있는 것을 구분해야 했어요.
공개 그룹은 가입 전에도 둘러볼 수 있게 했지만, 비공개 그룹은 계정으로 로그인하고 가입 승인을 받아야 해요. 초대 링크로 들어와도 이 과정을 건너뛰지는 않아요. 링크가 다른 사람에게 전달됐다는 이유만으로 비공개 글이 공개되면 안 됐어요. 화면에서 글을 숨기는 데 그치지 않고, 서버에서도 볼 수 있는 사람인지 확인하게 했어요.
친구를 부르는 기능에서 멤버를 관리하는 기능으로 넓어지니 관리자도 나눠야 했어요. 부관리자는 일반 멤버만 내보낼 수 있고, 그룹장은 다른 멤버에게 권한을 넘긴 뒤에 나가게 했어요. 그룹장이 그냥 나가 버려 아무도 관리할 수 없는 상황을 막으려는 결정이었어요.
멤버를 내보낼 때 그 사람이 쓴 글과 댓글까지 지울지도 정해야 했어요. 그룹에서 나가는 일과 과거의 대화를 지우는 일은 따로 두었어요. 탈퇴하거나 내보내져도 그룹에 남긴 글과 댓글을 자동으로 지우지는 않고, 비공개 그룹에 다시 들어오려면 승인을 받게 했어요.
대회 참여 버튼은 언제 사라져야 할까
대회는 시작 전, 접수 중, 투표 중, 종료 후로 나뉘어요. 접수 중일 때만 '대회에 참여하기'를 보여 주도록 했지만, 버튼을 숨기는 것만으로 끝나지는 않았어요. 접수 중에 화면을 열어 두었다가 마감 후에 게시할 수도 있으니까요. 파일을 올리는 동안 접수가 끝나는 경우도 있어서, 실제 게시물을 저장할 때 서버에서 시간을 다시 확인해야 했어요.
시간 비교에서 같은 시각을 어느 단계에 넣을지도 정했어요. 접수 마감 시각에 들어온 요청부터는 투표 기간으로, 투표 마감 시각부터는 종료로 봐요. 각 기기의 시계 대신 서버 시간을 쓰므로 같은 요청을 화면마다 다르게 판단하지 않아요.
여러 작품을 게시할 수 있다는 것과 여러 표를 행사할 수 있다는 것도 다른 조건이었어요. 대회 투표는 한 계정당 한 표로 제한하고, 본인 작품이나 숨겨진 작품에는 투표할 수 없게 했어요. 버튼을 빠르게 두 번 누르거나 요청이 동시에 와도 표가 두 개 저장되지 않도록 DB에서도 중복을 막았어요. 익명 프로필은 새로 만들기 쉬워서 대회 투표는 회원가입한 계정으로만 할 수 있게 했고요. 이것만으로 한 사람이 여러 계정을 만드는 문제까지 해결됐다고 볼 수는 없어요.
좋아요와 대회 투표를 구분하고, 대회 안에서 한 사람이 좋아요를 누를 수 있는 작품 수에도 한도를 두었어요. 결과를 낼 때는 공동 1등과 아무도 표를 받지 않은 대회도 나눠야 했어요. 모두 0표인데 공동 우승으로 표시하는 건 제가 원한 결과가 아니었어요. 최다 득표가 0보다 클 때만 우승자가 나오도록 했어요.
조건을 하나씩 추가하다 보니 정작 사진을 볼 화면이 설명문으로 채워졌어요. 서버에서 막을 조건은 그대로 두고, 화면에는 현재 할 수 있는 행동과 필요한 한도만 짧게 남겼어요.
사진을 고른 뒤 취소하거나 업로드가 실패한다면
사진을 선택해서 올리는 기능에도 중간 과정이 있었어요. 파일 선택 창을 열었다가 취소했을 때 기존 사진을 유지할지, 같은 파일을 다시 고를 수 있는지, 미리보기를 만드는 데 실패하면 무엇을 보여 줄지 확인했어요. 동영상까지 받으면서 파일 형식과 크기, 길이 제한도 함께 생각하게 됐어요.
파일을 저장하는 데 성공한 것과 게시물을 등록한 것은 같지 않았어요. 업로드가 끝나기 전에 게시 버튼을 누르거나, 올리던 파일의 유효 시간이 지나거나, 그사이에 그룹에서 나가게 될 수도 있어요. 게시할 때도 파일이 준비됐는지와 작성 권한을 확인해야 했어요.
실패한 뒤의 화면도 업로드 과정에 들어갔어요. 올리던 사진만 다시 시도하면 되는데 작성한 글까지 사라진다면 처음부터 다시 쓰게 돼요. 어느 단계에서 실패했는지 알려 주고 입력한 내용은 이어갈 수 있어야 했어요.
Game Mate는 친추판매와 코칭에서 시작했어요
Game Mate는 처음부터 친추판매와 코칭을 연결하는 서비스로 구상했어요. 제가 보던 클래시로얄 커뮤니티에서 해외 사용자들은 트위터를, 국내 사용자들은 카카오톡과 디시인사이드, 네이버 카페를 이용하고 있었어요.
해외 랭커와 친구를 맺기는 쉽지 않았고, 게임을 가르쳐 줄 사람을 찾는 글도 종종 봤어요. 요청하는 사람이 여러 플랫폼에 같은 글을 올리고 각각 확인해야 하는 모습이 불편해 보였어요. 흩어진 친추판매와 코칭 모집을 한곳에서 찾고, 조건을 확인한 뒤 대화할 수 있게 해 보고 싶었어요.
처음 사용자에게 다가갈 수 있는 곳도 제가 아는 클래시로얄 커뮤니티였어요. 그래서 여러 게임을 한꺼번에 시작하는 대신 이 게임부터 만들기로 했어요.
브롤스타즈나 롤도 생각했지만 처음부터 여러 게임을 다루지는 않기로 했어요. 클래시로얄 아래에 친추판매와 게임코치 게시판을 두고, 피드백, 친선전, 자유게시판도 함께 다루도록 했어요. 게임이 늘어나면 같은 분류의 게시판을 붙일 수 있게 만들었어요. 계정 거래는 위험 부담이 커서 뺐어요.
친추판매에는 금액을, 코칭에는 시간당, 기간 단위, 목표 달성까지의 수강료를 입력하게 했어요. 수강생이 코치를 찾을 수도 있고 코치가 수강생을 모집할 수도 있어요. 조건을 보고 채팅으로 이야기할 수 있게 하되, 서비스 안에서 결제하는 기능은 넣지 않기로 했어요.
금액을 입력하지 않은 경우도 생각해야 했어요. 빈칸을 0원으로 보여 주면 무료라고 오해할 수 있어서 '수강료 협의'로 표시하도록 했어요. 목표 달성형은 목표 점수와 그때까지의 총수강료를 받도록 했고요. '월 단위'보다는 '기간 단위', '목표 점수 달성'보다는 '목표 달성까지'가 어떤 조건으로 코칭을 받는지 더 잘 드러냈어요.
채팅에서 조건을 협의한 뒤에도 금액 지급과 환불은 이용자 사이에 남아요. 서비스가 결제를 처리하는 것처럼 보이면 제가 제공하지 않는 일까지 약속하게 되니, 모집과 대화까지로 범위를 정했어요.
사진 서비스와 Game Mate의 계정이나 데이터도 합치지 않았어요. 사진을 공유하러 오는 사람과 친추판매나 코칭을 찾는 사람은 이용 목적과 필요한 정보가 달랐어요. 같은 저장소에서 만든다고 같은 계정으로 써야 할 이유는 없었어요.
피드백 게시판에서는 누가 답해줄까
친추판매와 코칭 외에 피드백 게시판도 만들면서, 질문에 답해줄 사람을 생각해야 했어요. 게임 프로필 연동이나 다국어 기능이 있어도 글을 올렸는데 아무 답도 없으면 다시 들어올 이유가 없으니까요.
고민을 올린다
→ 다른 사람이 답변한다
→ 요청자에게 답변을 알린다
→ 요청자가 읽고 도움 여부를 표시한다질문과 답변을 저장해도 요청자가 답변을 못 보면 대화가 이어지지 않아요. 답변 알림으로 다시 들어오게 하고, 읽은 뒤에는 도움됨을 표시할 수 있게 했어요.
우선 도움됨으로 표시한 답변이 하나라도 있는 요청이 얼마나 되는지 보려고 해요. 같은 요청에 유용한 답변이 여러 개 달려도 요청 한 건으로 세어요. 첫 답변까지 걸린 시간과 24시간 안에 답변을 받은 비율도 같이 보려고 해요.
아직 답변해 줄 사람을 모집하지는 못했어요. 먼저 5~10명과 기존 커뮤니티의 질문을 수동으로 연결해 보는 걸 생각하고 있어요.
전송 실패라고 떠도 메시지는 도착했을 수 있었어요
채팅도 내용을 저장하고 상대에게 보여 주면 끝일 것 같았어요. 하지만 서버에는 저장됐는데 응답을 받기 전에 연결이 끊기면, 보낸 사람의 화면에서는 실패로 보일 수 있어요. 이때 다시 보내기를 누르면 같은 말이 두 번 도착할 수 있었어요.
그래서 같은 메시지의 재전송에는 같은 요청 식별자를 쓰고, 서버에서는 이미 저장한 메시지가 있으면 그 결과를 돌려주도록 했어요. 같은 식별자를 다른 대화나 다른 내용에 재사용하는 것은 막았어요. '다시 보내기'가 새 메시지를 하나 더 만드는 버튼이 되어서는 안 됐어요.
메시지를 한 번 저장하는 일과 읽었다고 표시하는 일도 나눠 봤어요. 채팅방에 들어온 동안 새 메시지가 도착할 수 있으니, 입장했다는 이유로 전부 읽음으로 바꾸지는 않아요. 클라이언트가 확인한 메시지까지만 처리해요. 대화내역을 지울 때도 내 화면에서 지운 범위만 적용하고 상대방의 기록은 남겨요.
로그아웃하고 다른 계정으로 들어오는 경우도 확인했어요. 서버의 로그인 상태만 바꿔서는 이전 계정으로 받아 둔 목록이나 늦게 도착한 응답이 화면에 남을 수 있었어요. 계정이 바뀔 때 진행 중인 조회를 취소하고 이전 조회 결과를 비우는 처리도 필요했어요.
삭제와 차단은 다른 사람의 화면에도 영향을 줬어요
글 하나를 지우는 기능도 작성자의 화면만 보면 단순했어요. 그런데 그 글에 다른 사람이 답변이나 댓글을 남겼다면 같이 지워도 되는지부터 결정해야 했어요. 댓글을 지울 때 다른 사람의 답글까지 없어지는 것도 원하지 않았어요.
Game Mate에서는 원댓글을 지워도 다른 사람의 답글은 남기도록 했어요. 회원 탈퇴 때도 탈퇴한 사람이 남긴 본문과 개인정보를 정리하는 일과, 다른 사람이 남긴 답변과 댓글을 보존하는 일을 구분했어요. 운영자가 숨긴 글이라도 작성자가 자기 내용을 삭제할 수 있어야 한다는 경우도 확인했어요.
차단은 프로필에서 버튼 하나를 누르는 기능이었지만, 그 뒤에 새 대화를 시작할 수 있는지와 기존 대화에서 메시지를 보낼 수 있는지도 함께 봐야 했어요. 상대방이 먼저 요청을 보내거나 화면을 거치지 않고 API를 호출해도 채팅 제한이 적용되게 했어요. 신고 버튼을 만드는 것과 신고된 내용을 누가 확인하고 어떤 조치를 할지 정하는 것도 별개의 일이었어요.
차단은 API에서 바로 제한할 수 있지만, 신고는 내용을 보고 조치할 사람이 필요해요. 어떤 내용을 어느 기준으로 처리할지는 신고 버튼 이후에도 운영에서 다룰 일이 남아요.
게임 프로필을 불러왔다고 본인 계정은 아니었어요
게임 계정 연결은 Player Tag를 입력해서 프로필을 불러오도록 만들었어요. 그런데 다른 사람의 태그를 넣어도 조회는 돼요. 프로필을 불러오는 데 성공했다고 본인 계정이라는 인증 표시를 붙이면 안 됐어요.
그래서 연결 직후에는 소유권 미확인으로 두었어요. 운영자가 확인한 사용자라는 표시와 게임 계정의 소유권도 별개로 다뤄요. 태그 조회에 성공했다는 이유만으로 인증 표시를 붙이지 않도록 한 거예요.
처음에는 글을 쓸 때 덱과 게임 정보를 스냅샷으로 남기려고 했어요. 작업하면서 게임 프로필은 사용자 계정에 등록하는 쪽으로 정리하고, 글쓰기 화면의 덱 첨부는 뺐어요. 프로필 인증을 요청할 때는 관리자와 1:1 채팅을 시작하도록 했어요.
인증 이후에 대표 게임 태그를 바꾸는 경우도 있었어요. 예전 태그로 확인한 표시를 새 태그에 그대로 붙일 수는 없어서, 대표 태그가 바뀌면 기존 인증을 해제하고 다시 확인하게 했어요. 외부 게임 API가 응답하지 않거나 조회할 수 없는 태그를 넣은 상황도, 닉네임만으로 가입하는 과정과는 따로 처리해야 했어요.
가입할 때 안 해도 되는 것들
게임 프로필은 가입할 때 꼭 연결하지 않아도 되게 했어요. 바로 연결하고 싶은 사람은 태그를 조회하고, 나중에 하고 싶으면 닉네임과 필수 동의만으로 가입할 수 있어요.
입력할 게 길어져서 본문만 스크롤하고 가입 완료 버튼은 아래에 남도록 바꿨어요. 높이가 짧은 화면에서도 입력과 버튼이 겹치지 않는지 확인했어요.
프로필을 펼침 메뉴에서 수정하는 방식도 마음에 들지 않았어요. 처음에는 '프로필 수정'을 누르면 Bottom Sheet가 열리도록 고쳤고, 이후 Game Mate에서는 수정 내용을 한곳에서 다룰 수 있게 별도 페이지로 옮겼어요. 자기소개에 적은 웹 주소는 프로필에서 누를 수 있게 했고요.
프로필을 수정하다 그만두는 동작도 같은 화면에서 찾을 수 있어야 했어요. 모바일에서 올라오는 창에는 X 대신 취소와 저장을 나란히 두었어요. 본문은 스크롤하되 마지막 입력칸과 버튼은 작은 화면에서도 닿도록 했어요.

공개 프로필에는 연결한 게임 계정이 보이고, 수정은 별도 페이지에서 해요. 테스트 계정과 가짜 게임 API 응답으로 확인한 화면이에요.
버튼과 입력, 모달은 두 서비스가 함께 쓰는 디자인 시스템으로 만들었어요. 사진 서비스에서는 사진을 크게 보는 게 중요했고, Game Mate에서는 게임 종류와 작성자의 상황이 잘 읽혀야 했어요. 공통 UI를 쓰면서도 각 화면의 배치와 여백은 따로 고쳤어요.
뒤로 가면 어디로 돌아가야 할까
화면을 순서대로 눌러서 들어갈 때만 생각하면 뒤로 가기도 쉬워 보였어요. 하지만 게시물 링크를 직접 열거나, 다른 사이트에서 공유 링크로 들어온 사람에게는 이전 화면이 없을 수 있어요. 브라우저 기록만 따라가면 서비스 밖으로 나가 버릴 수도 있었어요.
그룹 안에서 대회 상세를 열면 헤더에는 그룹명과 대회명을 보여 주고, 뒤로 가기는 그룹으로 이어지도록 요청했어요. 대회 상세에 그룹의 게시물, 대회, 명예의 전당 탭까지 계속 보여 주는 건 현재 위치를 헷갈리게 했어요. 공개 프로필도 헤더에서 돌아갈 곳을 알려 주고, 본문은 실제 프로필을 보여 주는 데 쓰도록 바꿨어요.
로그인이 필요한 행동도 화면을 떠나는 과정이었어요. 바로 로그인으로 보내기보다 이유를 알리고, 취소하면 그 자리에 남게 했어요. 로그인한 뒤에는 원래 보고 있던 곳으로 돌아와야 했고요.
게시글만 번역하면 답변은?
Game Mate는 한국어, 영어, 일본어를 생각하고 만들었어요. 처음에는 메뉴와 게시글을 번역하면 될 줄 알았어요.
그런데 한국어 질문을 일본어로 읽고 답변한 사람이 있으면, 질문한 사람은 그 일본어 답변을 못 읽을 수도 있었어요. 피드백을 주고받으려면 답변도 번역해야 했어요.
언어별 경로는 먼저 만들었지만 번역 기능과 각 언어권 운영은 아직 확인할 게 남아 있어요. 모바일도 웹 화면을 함께 쓰는 WebView로 만들었어요. 우선 웹에서 질문하고 답변을 확인하는 걸 다듬고, 앱 배포와 스토어 운영은 그다음에 하려고 해요.
블로그 소개에 이력서까지 넣을 필요는 없었어요
블로그 글은 원래 별도의 저장소에서 관리하고 있었어요. 글을 티스토리로 옮겨 발행하는 게 번거로웠고, 직접 블로그를 만드는 비용도 예전보다 낮아져서 제 블로그를 만들기로 했어요.
만드는 과정에서 AI 에이전트가 요청한 범위를 넘어 이력서까지 만들어둔 부분도 있었어요. 이력서는 제가 없애고, 소개 화면에는 연락처만 남겼어요.
한국어와 영어 글도 실제 번역이 있는 경우에만 해당 언어로 볼 수 있게 했어요.
언어를 바꿔 보다가 테마 초기화 스크립트가 다시 렌더링되며 경고를 내는 문제도 찾았어요. 글만 바뀌는지 보면 놓칠 수 있는 동작이었어요. 다크 모드를 선택한 채 언어를 바꾸고, 주소로 바로 들어오는 경우와 사이트 안에서 이동하는 경우를 비교하면서 고쳤어요.
아키텍처를 정해 둬도 코드는 다시 읽어야 했어요
화면을 써 보는 것과 함께 구현도 검토했어요. 기능별로 폴더를 나눠 놓아도 다른 기능의 내부 파일을 바로 가져오면, 수정할 때 어디까지 영향을 주는지 다시 찾아야 했어요. 이런 참조는 각 기능의 공개 진입점을 거치도록 고치고, 화면 조합과 조회 코드를 어디에 둘지도 다시 맞췄어요.
같은 서버 데이터를 여러 곳에서 따로 관리하는 부분도 살폈어요. 서버에서 받아 온 값은 Apollo나 TanStack Query가 관리하고, 입력 중인 값은 폼이 맡도록 했어요. 조회 결과를 별도의 상태로 복사해 두면 저장 뒤 캐시는 바뀌었는데 화면에는 이전 값이 남는 식의 문제를 만들기 쉬웠어요.
코드를 감쌌다고 항상 읽기 편해지는 것도 아니었어요. 라이브러리의 조회 훅을 한 번 호출해서 그대로 돌려주는 훅이나, 인자를 그대로 넘기기만 하는 콜백은 필요한지 다시 봤어요. 요청과 캐시 갱신을 함께 정하는 옵션은 남기고, 호출을 한 번 더 따라가게 할 뿐인 래퍼는 줄였어요.
오류 처리도 경계가 있다는 것만으로 끝나지 않았어요. 사진 서비스에서는 게시물 목록 조회가 실패한 뒤 다른 카테고리를 골라도 이전 오류가 남는 경우를 고쳤어요. 필터는 계속 사용할 수 있게 두고, 선택한 그룹이나 대회, 카테고리가 바뀌면 해당 목록의 오류도 초기화하도록 했어요. 정상적으로 목록이 나오는 화면만 봤다면 놓쳤을 동작이었어요.
이렇게 검토한 내용을 다음 요청에서 매번 다시 설명하기는 번거로웠어요. 반복해서 적용할 코드 기준은 AGENTS.md와 작업별 스킬에 남기고, import 경계나 상태를 중복으로 관리하는 패턴처럼 검사할 수 있는 부분은 ESLint로 확인하게 했어요. 코드를 어디에 둘지 적어 주는 것과 실제로 그 기준에 맞게 작성됐는지 확인하는 작업이 함께 필요했어요.
API와 캐시를 고른 이유, 서버와 DB에서 확인한 내용은 두 사이드 프로젝트에서 기술을 선택한 기준에 따로 적었어요.
검색으로 들어오는 화면과 캐시도 확인했어요
서비스 안에서 버튼을 눌러 보는 것 외에, 검색해서 글을 찾고 링크로 들어오는 경우도 살폈어요. Game Mate의 공개 게시글에는 제목과 설명, 대표 주소인 canonical, 공유할 때 쓸 이미지와 구조화 데이터를 넣었어요. 검색 엔진이 자바스크립트를 실행하지 않아도 글과 댓글을 읽을 수 있는지 확인했고, 사이트맵에는 공개 글과 프로필을 담았어요. 채팅이나 설정 화면은 검색에 나오지 않도록 구분했어요.
캐시는 어디에 적용하고 언제 비울지까지 정해야 했어요. Game Mate의 공개 게시글은 로그인 쿠키 없이 받은 내용을 함께 캐시해요. 로그인한 사람이 차단한 작성자의 댓글을 보게 되는 일은 없어야 해서, 사용자별로 제외할 내용은 따로 확인해요. 개인별 결과를 모두가 쓰는 캐시에 넣지 않도록 나눈 거예요.
글을 수정하거나 댓글을 남긴 뒤에는 관련 캐시를 만료시키도록 했어요. API에서 저장에 성공했는데 캐시 갱신 요청만 실패할 수도 있어서, 다시 갱신할 항목을 DB에 남기고 재시도하게 했어요. 브라우저 안의 조회 결과도 함께 갱신해야 저장한 내용이 바로 보여요. 화면을 빠르게 띄우는 것뿐 아니라, 바뀐 내용을 언제 다시 보여 줄지도 검토했어요.
오류가 났을 때 남길 로그도 골랐어요
로컬 화면에서 확인한 오류는 바로 볼 수 있지만, 배포한 뒤 생긴 오류는 어떤 코드에서 났는지 기록이 있어야 찾아볼 수 있어요. 웹과 API에 Sentry를 연결하고 서비스와 배포 버전을 구분하도록 했어요. 입력 조건을 맞추지 못한 요청과 예상하지 못한 서버 오류도 구분해서 수집하도록 했고요.
오류를 찾으려고 사용자가 쓴 글이나 채팅까지 보내고 싶지는 않았어요. API 오류를 수집할 때는 요청 본문과 쿠키, 헤더를 제외하고, 오류 메시지에 Prisma의 조회 인자가 섞일 수 있어 오류 종류와 코드, 호출 위치를 남기도록 바꿨어요. 로그가 남는지만 보는 대신, 그 안에 어떤 정보가 들어가는지도 확인했어요.
만들고 나서 다시 정한 게 많았어요
에이전트는 제가 적지 않은 부분까지 채워서 결과물을 만들었어요. 홈을 그룹 목록으로 보여 준 것과 블로그에 이력서를 만든 것도 그 과정에서 나온 선택이었어요. 그중 무엇을 남기고 바꿀지는 직접 써 보며 정했어요.
익명 기록이나 그룹장이 나가는 순서처럼 처음부터 제가 더 정해 줬어야 할 것도 있었어요. 화면이 나온 뒤에야 보인 불편도 있었고, 실행은 되지만 코드를 읽다가 다시 고친 부분도 있었어요. 기능을 추가하는 요청만큼 이미 나온 화면과 코드를 검토하고 수정할 내용을 정하는 일이 많았어요.
배포 쪽 이야기는 일단 EC2 한 대에 올려보기로 했어요에 따로 적었어요.


