수정

혼자 서비스를 운영한다는 것

회사에서 제품을 만들고 여러 사이드 프로젝트를 운영하면서 서비스를 알리는 일이 어렵다고 자주 느꼈다. 사용자가 찾아온 뒤에도 풀리지 않는 질문이 있었다. 왜 돈을 내고 쓰는지, 갑자기 가입자가 늘었다면 어디에서 무엇을 보고 들어왔는지 알고 싶었다. 그래야 무엇을 고치고 어디에 힘을 쏟을지 정할 수 있었다.

아이두를 만들 때도 그랬다. 내가 쓰려고 만든 서비스에 누군가 결제했을 때는 기쁘면서도 의외였다. 어떤 불편을 해결해 줬기에 돈을 냈을까 궁금했다. 외국인 사용자의 가입을 보고 다국어 지원을 시작하기도 했다. 쓰는 사람을 알아가면서 처음에는 생각하지 못했던 방향으로 제품을 바꾸게 됐다.

이런 경험이 쌓일수록 유입과 사용 과정을 기록할 필요를 느꼈다. 지표로 어디에서 변화가 생겼는지 찾고, 사용자의 이야기를 들으며 결제하거나 떠난 이유를 이해하고 싶었다. 잘된 시도를 이어 가고 막힌 부분을 고치려면 필요한 일이었지만, 꾸준히 추적하기는 어려웠다.

인원이 적을수록 더 그렇다. 기능을 만들고 문의에 답하는 사이에 홍보할 글과 이미지를 준비해야 한다. 여러 SNS에 게시한 뒤에는 반응과 유입도 살펴야 한다. 일이 생길 때마다 다른 화면을 열고 데이터를 모으다 보니, 혼자 이 과정을 계속 챙기는 데 한계를 느꼈다.

모여는 이런 부담을 줄이려고 만든 서비스다. SNS 게시물 작성과 예약, 댓글 응대, 성과 확인을 한곳에서 처리하고 브랜드를 소개하거나 문의를 받는 소개 페이지도 제공한다. 홍보한 결과를 보고 다음 작업까지 이어 가도록 돕고 싶었다. 기획과 디자인부터 프론트엔드, 서버, 운영까지 혼자 맡고 있다.

모여는 2026년 10월 14일 정식 출시했다. Instagram과 Threads 권한 심사, Google 로그인 심사를 마쳤고 YouTube API 할당량 심사는 진행 중이다. moyeo.io에서 바로 써 볼 수 있다.

홈에는 연결한 채널의 상태와 오늘 할 일을 함께 두었다. 답하지 않은 댓글과 최근 반응을 확인한 뒤 다음 게시를 준비한다. 여러 앱에서 따로 하던 일을 이곳에서 이어서 처리하도록 구성했다.

모여테스트 워크스페이스의 홈. 연결된 채널과 오늘 할 일을 함께 본다.
모여테스트 워크스페이스의 홈. 연결된 채널과 오늘 할 일을 함께 본다.

처음부터 이런 제품을 생각한 것은 아니다. 출발점은 AI 카드뉴스 제작 도구였다. 방향을 바꾸는 동안 누구에게 필요한지, 얼마를 받아야 하는지, 어떤 숫자를 보고 다음 결정을 내릴지 다시 생각하게 됐다. 이번 글에는 그 과정에서 고민한 제품과 운영의 기준을 정리했다.


카드뉴스에서 시작한 모여

처음에는 로고와 캐릭터, 제품 사진을 넣고 이야기를 짧게 적으면 브랜드에 맞는 카드뉴스를 만드는 도구를 생각했다. 긴 프롬프트를 작성하고 결과를 편집기로 옮기는 수고를 줄이고 싶었다.

여러 제작 도구를 써 봤지만 내가 원한 조건을 한 번에 만족시키기는 어려웠다. 한국어 문구가 깨지거나, 카드마다 이미지 분위기가 달라지거나, 마음에 들지 않는 부분만 고치기 어려웠다. 이미지 한 장의 완성도와 실제 홍보에 바로 쓸 수 있는 카드 묶음의 완성도는 달랐다.

아래는 당시 만들었던 편집기다. 카드를 직접 편집하고, 선택한 부분을 AI에 수정 요청하고, 게시 전에 미리 보는 흐름까지 구현해 보았다.

초기 카드뉴스 프로토타입. 카드 편집과 선택한 문구의 AI 수정을 한 화면에 두었다. 현재 모여와는 다른 초기 구상이다.
초기 카드뉴스 프로토타입. 카드 편집과 선택한 문구의 AI 수정을 한 화면에 두었다. 현재 모여와는 다른 초기 구상이다.

문구를 이미지 모델에 함께 맡기는 방법과 배경을 생성한 뒤 텍스트를 별도로 얹는 방법을 고민했다. 전자는 모델의 표현력에 기대는 비중이 크고, 후자는 글자를 안정적으로 표시하고 수정하기 좋다. 대신 배치와 줄바꿈을 직접 구현하고, 이미지와 글이 어울리도록 조정해야 했다.

아래는 당시 카드뉴스 서비스 프로토타입으로 만든 실제 결과물이다. 카페 홍보용 이미지에 문구를 넣고, 마지막에는 게시물 저장이나 방문을 유도하는 내용을 담았다.

초기 카드뉴스 서비스 프로토타입으로 만든 실제 결과물.
초기 카드뉴스 서비스 프로토타입으로 만든 실제 결과물.

다시 만드는 비용

비용도 문제였다. 다섯 장 중 두 장만 마음에 들면 인물이나 분위기를 맞추려고 다시 만들게 된다. 글자를 따로 얹어도 이미지 자체를 재생성하는 비용은 남는다.

아래는 모델과 설정을 고정한 정사각형 이미지의 출력 비용 예시다. 다섯 장을 만든 경우와 두 번 다시 만들어 총 열다섯 장을 생성한 경우를 비교했다. 과거 청구액이 아닌 재생성 원가를 가늠하기 위한 계산이다.

모델과 설정5장15장
Gemini 3.1 Flash Image, 1K약 469원약 1,407원
GPT Image 2, 1024×1024, Medium약 371원약 1,113원
GPT Image 2, 1024×1024, High약 1,477원약 4,431원

장당 출력 단가는 각각 0.067달러, 0.053달러, 0.211달러이고, 환율은 계산 편의를 위해 달러당 1,400원으로 가정했다. 입력 텍스트와 참조 이미지, 저장·전송 비용, 세금은 별도다. 모델과 설정별 화질이 같다는 의미도 아니다. 단가는 Google 가격표와 OpenAI 이미지 생성 비용 안내를 참고했다.

원가를 계산할 때 기준으로 삼아야 할 것은 사용자가 만족해서 가져간 결과 한 세트였다. 몇 번까지 다시 만들게 할지, 실패하면 사용량을 돌려줄지, 언제 환불할지 정해야 가격을 붙일 수 있었다. 실패한 요청에도 요금이 붙는지는 공급자 정책과 실패 단계에 따라 따로 봐야 한다.

방향을 바꾼 이유

그사이 이미지 모델도 빠르게 발전했다. 프로토타입을 만들 때는 결과가 아쉬웠는데, 시간이 지나자 프롬프트를 조금만 다듬어도 괜찮은 이미지가 나왔다. 고객이 이미 사용하는 범용 AI 도구에서 충분한 이미지를 얻는다면, 별도의 제작 서비스에 비용을 더 낼 이유는 무엇일까.

브랜드 맥락을 보존하거나 편집 시간을 줄이는 것처럼 추가로 해결하는 일이 있어야 했다. 구독형 AI에도 사용 한도는 있지만, 고객에게는 이미 지불한 도구로 작업을 끝내는 선택지가 있었다.

돌아보면 처음에는 원하는 결과물과 만드는 방법을 먼저 구체화했다. 누가 이 작업을 얼마나 자주 하고, 지금 어떤 방식으로 해결하며, 무엇에 돈을 낼지는 충분히 좁히지 못했다. 이미지 품질을 높이는 데 시간을 더 쓰기 전에, 내가 감당할 원가 안에서 고객이 반복해서 선택할 이유를 먼저 확인할 필요가 있었다.

그래서 카드뉴스 제작 중심의 구상은 프로토타입 단계에서 접었다. 내가 직접 겪었던 홍보 과정을 돌아보니 이미지를 만든 뒤에도 할 일이 많았다. 어디에 언제 올릴지 정하고, 채널마다 내용을 조정하고, 질문에 답하고, 다음에 무엇을 올릴지 판단해야 했다.

콘텐츠를 만드는 순간에서 꾸준히 운영하는 과정으로 관심을 넓혔다.
콘텐츠를 만드는 순간에서 꾸준히 운영하는 과정으로 관심을 넓혔다.

모여는 사용자가 다른 도구에서 준비한 콘텐츠를 가져와 게시하고, 댓글에 답하고, 다음 홍보를 준비하는 서비스로 방향을 바꿨다. 이미지 생성 모델이 좋아지면 그 결과물을 모여로 가져와 쓰면 된다. 내가 풀고 싶은 문제는 콘텐츠를 만든 뒤에 남아 있는 운영의 번거로움이었다.

PMF(Product-Market Fit)는 제품이 시장의 수요와 맞아 지속적으로 선택받는 상태를 뜻한다. 당시에는 그 단계에 이르기 전에 원가와 대체재 문제를 발견했다. 방향을 바꾼 뒤에는 모여가 해결하려는 일이 실제로 반복되는지, 고객이 그 해결에 돈을 낼 의향이 있는지부터 확인해야 했다.


다시 쓰는 사용자를 찾아서

먼저 만나고 싶은 고객은 SNS를 직접 운영하면서 다른 업무도 함께 맡는 1인 사업자와 작은 팀이다. 자기 상품이나 서비스를 잘 알아도 홍보에 쓸 시간은 부족하기 쉽다. 콘텐츠를 만든 뒤 여러 채널을 오가며 일을 마무리하는 데에도 시간이 든다.

개발 동아리를 운영하고 교육을 진행하며 약 2,000명을 만났다. 그중 지금도 연락하는 분들과 초기 스타트업을 운영하는 대표님들에게 제품을 보여 주고 의견을 들을 기회가 있었다.

제품을 처음 보고 좋다고 말해 주는 것도 고마웠지만, 더 알고 싶은 것은 며칠 뒤에도 쓰는 이유였다. 먼저 써 본 분들에게 어떤 일을 반복해서 맡기는지, 한 번 써 보고 멈췄다면 어디가 불편했는지를 물었다. 소수라도 꾸준히 쓰는 사람이 있다면 그 사람의 업무에서 모여가 어떤 역할을 하는지 이해하고 싶었다.

이후에도 의견을 들을 때는 최근에 했던 작업을 함께 되짚어 보려 한다. 어떤 콘텐츠를 어느 채널에 올렸는지, 중간에 다른 앱을 열어야 했는지 물으면 구체적인 불편을 찾기 좋다. 새 기능을 요청받았을 때도 먼저 그 기능이 필요해진 상황을 알아보려 한다.

  • 최근 어떤 일을 끝냈는가? 실제 사용한 흐름과 기대했던 결과를 확인한다.
  • 중간에 어디에서 멈췄는가? 연결, 작성, 예약, 결과 확인 중 어려운 지점을 좁힌다.
  • 다시 사용한 이유는 무엇인가? 업무에 계속 필요한 기능과 한 번 살펴본 기능을 구분한다.
  • 기존 방식으로 돌아갔다면 왜인가? 익숙함, 품질, 시간, 가격 중 어떤 문제가 남았는지 묻는다.

그다음에는 비슷한 고객도 겪는 문제인지, 모여가 해결하려는 일과 맞는지 살핀다. 혼자 만드는 만큼 구현한 뒤에 유지할 시간까지 생각해야 한다.

지금은 첫 게시를 마친 사람이 다음 주에도 게시하는지, 성과를 본 뒤 새 콘텐츠를 준비하는지, 소개 페이지로 들어온 문의를 처리하는지 살피는 단계다. 이런 작업이 반복되는 모습을 봐야 모여를 계속 쓰는 이유와 유료로 바꿀 이유가 구체적으로 보인다.

처음 써 보는 것, 다시 찾는 것, 비용을 지불하는 이유는 각각 확인해야 한다.
처음 써 보는 것, 다시 찾는 것, 비용을 지불하는 이유는 각각 확인해야 한다.

이 관점으로 게시물 작성부터 성과 확인까지의 흐름을 다시 살폈다. 한 번의 게시를 끝낸 뒤에도 다음 작업이 모여 안에서 이어져야 했다.


게시 다음에 할 일

SNS마다 본문 길이와 사진 수, 동영상 조건이 다르다. 같은 글을 여러 번 복사하는 수고를 줄이되, 게시 전에 채널별 조건은 보여 줘야 했다.

작성 화면에서는 공통 본문과 이미지를 준비한 뒤 채널별로 조정한다. 아래는 모여 소개 게시물을 작성하는 화면이다. Instagram과 Threads를 함께 선택하고 게시될 모습을 옆에서 확인한다.

여러 채널을 선택한 작성 화면. 공통 내용에서 시작해 채널별로 다듬고, 게시될 모습을 미리 확인한다.
여러 채널을 선택한 작성 화면. 공통 내용에서 시작해 채널별로 다듬고, 게시될 모습을 미리 확인한다.

예약할 때는 채널과 시간대를 정하고 게시 결과까지 확인하게 했다. API가 지원하는 형식과 권한 안에서 처리하며, 앱에서 직접 마무리해야 하는 요소는 설명한다. 외부 도구가 앱의 기능을 모두 그대로 제공하지는 못하기 때문이다.

채널별 캘린더. 같은 주의 Threads·Instagram 게시물을 비교하고, 게시물이 없는 채널도 함께 확인한다.
채널별 캘린더. 같은 주의 Threads·Instagram 게시물을 비교하고, 게시물이 없는 채널도 함께 확인한다.

모여에서 관리한 게시물과 외부 채널에서 가져온 게시물은 출처를 구분한다. 상태는 예약, 게시 완료, 확인 필요, 실패로 나누고 달력을 보는 시간대도 표시했다.

게시한 다음에는 댓글을 확인해야 한다. 그래서 댓글함에 원래 게시물과 답변 필요 상태, 답글 입력란을 함께 두었다. 어떤 글에 달린 질문인지 다시 찾아보지 않고 답변을 이어 가도록 했다.

댓글함. 게시물과 반응을 살피면서 답변이 필요한 댓글을 고르고, 같은 화면에서 답글을 작성한다.
댓글함. 게시물과 반응을 살피면서 답변이 필요한 댓글을 고르고, 같은 화면에서 답글을 작성한다.

성과를 보고 다음 작업으로

여러 채널의 숫자를 한곳에 모아도 무엇을 할지 모르면 사용자는 화면을 다시 해석해야 한다. 그래서 성과를 확인한 자리에서 반응이 좋았던 게시물을 열고, 다음 글을 준비하거나 밀린 답변을 처리하게 했다. 숫자를 본 다음 할 일로 바로 넘어가게 하는 데 신경을 썼다.

성과 화면에서는 연결한 SNS 채널의 조회수와 반응, 댓글 응답률을 함께 본다.

기간별 반응 흐름 아래에 이번 주 할 일을 배치했다. 답글 작성과 다음 게시물 준비로 바로 이어진다.
기간별 반응 흐름 아래에 이번 주 할 일을 배치했다. 답글 작성과 다음 게시물 준비로 바로 이어진다.

그래프 아래에는 답변이 필요한 댓글, 이번 기간에 반응이 좋았던 게시물, 앞으로 예약한 게시물을 함께 두었다. 조회수를 확인한 뒤 다시 메뉴를 찾아다니는 대신 그 자리에서 작업을 시작하는 구조다.

플랫폼마다 제공하는 지표와 수집 시점이 달라 어디까지 비교할 수 있는지도 설명해야 했다. 제공하지 않는 값은 0으로 채우지 않고, 마지막 수집 시각과 집계 대기 상태, 해당 지표의 채널 범위를 표시했다.

성과 요약과 채널 비교. 전체 수치가 움직였을 때 어느 채널과 게시물을 더 살펴볼지 좁힌다.
성과 요약과 채널 비교. 전체 수치가 움직였을 때 어느 채널과 게시물을 더 살펴볼지 좁힌다.

추천 게시 시간도 표본과 함께 읽어야 한다. 몇 편의 반응이 좋았다고 그 시간이 늘 유리하다고 단정하기는 어렵다. 그래서 정답이 아니라 사용자가 후보 시간을 골라 시험해 볼 근거로 보여 준다.

댓글 응답률과 추천 게시 시간. 시간대별 반응과 표본 수를 보고 다음 게시를 준비한다.
댓글 응답률과 추천 게시 시간. 시간대별 반응과 표본 수를 보고 다음 게시를 준비한다.


SNS에서 문의까지

SNS 게시물을 보고 관심이 생긴 사람은 프로필 링크를 누른다. 그런데 열린 페이지에서 무엇을 하는 곳인지, 어떻게 문의하는지 찾기 어렵다면 홍보는 거기서 끊긴다. 소개 페이지는 브랜드 설명과 실제 행동 사이를 이어 주려고 넣었다.

사용자는 블록을 조합해 상품과 연락 방법, 위치, 예약 신청을 한 페이지에 담는다. 방문자가 원하는 날짜와 연락처를 남기면 운영자가 일정을 조율하는 방식이다.

소개 페이지의 방문자 화면. 브랜드 설명에서 상담 예약 신청과 연락, SNS 채널로 이어진다.
소개 페이지의 방문자 화면. 브랜드 설명에서 상담 예약 신청과 연락, SNS 채널로 이어진다.

실제 화면은 모여 공식 소개 페이지에서 열어 볼 수 있다. 편집할 때는 방문자에게 보일 모습을 옆에서 확인한다.

블록을 편집하면서 방문자에게 보일 모습을 함께 확인한다.
블록을 편집하면서 방문자에게 보일 모습을 함께 확인한다.

쓰던 페이지에서 옮겨 오기

링크 모음 페이지를 만드는 일 자체는 어렵지 않다. 작은 가게를 꾸리는 사람에게 더 어려운 건 여러 홍보 중 무엇이 통했는지 아는 쪽이다. 다만 이미 리틀리나 링크트리, 인포크링크를 쓰고 있다면 옮기는 일부터 부담이라, 쓰던 페이지 주소만 넣으면 링크를 찾아오게 했다.

다른 서비스의 링크 가져오기. 쓰던 링크 모음 주소를 넣고 링크를 찾는다.
다른 서비스의 링크 가져오기. 쓰던 링크 모음 주소를 넣고 링크를 찾는다.

서버가 공개 프로필에서 링크를 꺼내 최대 50개까지 보여 주고, 사용자가 고른 링크만 페이지에 붙인다. 사용자가 넣은 주소로 서버가 직접 요청하는 기능이라 SSRF도 막아야 했다. https와 공인 IP만 허용하고 리다이렉트마다 다시 검사하며, 5초나 1MiB를 넘으면 멈춘다.

방문 한 번을 어떻게 셀까

분석이 핵심이라면 숫자부터 믿을 수 있어야 한다. 운영자가 미리보기를 열 때마다 방문이 늘거나 링크 미리보기 봇이 손님으로 잡히면 성과 화면이 통째로 틀어진다. 그래서 무엇을 세지 않을지부터 정했다.

봇과 브라우저가 미리 불러온 요청(Sec-Purpose), 모여에 로그인한 채 연 페이지, 편집기 미리보기는 세지 않는다. 방문도 페이지가 실제로 화면에 보일 때만 보낸다. 그다음 기준은 아래와 같다.

무엇을어떻게왜
같은 탭 새로고침sessionStorage로 탭마다 한 번만 센다새로고침이 방문을 부풀리지 않게
다시 온 방문자localStorage에 마지막 방문일을 30일 두고 서버에는 참·거짓만 보낸다식별자 없이 재방문만 알려고
블록 노출블록이 50% 이상 보이면 방문마다 한 번스쳐 지나간 블록을 빼려고
링크 클릭서버의 /go 리다이렉트를 거친 클릭만 센다실제로 이동한 클릭만 남기려고
머문 시간페이지가 가려질 때 보내고 30분에서 자른다열어 둔 탭이 평균을 흔들지 않게

IP와 User-Agent를 해시해 방문자를 묶는 방법은 일부러 쓰지 않았다. 한국 이동통신망은 CGNAT로 많은 사람이 같은 공인 IP를 나눠 써서, 서로 다른 손님이 한 명으로 합쳐질 수 있다. IP는 요청 횟수를 제한하는 데만 쓰고 저장하지 않는다.

쿠키도 쓰임을 좁혔다. 세션 쿠키 moyeo-bio-source는 처음 들어온 경로를 기억했다가 같은 방문의 클릭과 신청을 그 경로로 이어 기록한다. moyeo-bio-visitor는 링크 제목 A/B 테스트의 그룹을 나눌 때만 읽고 통계에는 쓰지 않으며, 개인정보 처리방침에도 그렇게 적었다.

어디에서 왔는가

방문 경로는 utm_* 파라미터와 Referer처럼 확실한 단서부터 보고, ChatGPT나 Perplexity에서 온 방문은 AI 검색으로 나눈다. 그래도 모르면 인앱 브라우저의 User-Agent를 보고, 아무 단서가 없을 때만 직접 방문으로 센다.

그런데 Instagram 앱에서 열린 방문은 프로필 링크에서 왔을까, DM에서 왔을까? 이런 단서만으로는 가리기 어렵다. 그래서 프로필과 DM 자동 응답, Threads, 네이버 스마트플레이스, QR처럼 링크를 붙일 자리마다 코드가 다른 공유 링크를 만들게 했다. 코드가 붙은 방문은 추측보다 코드를 따른다.

유입 경로별 공유 링크. 같은 소개 페이지라도 링크를 붙인 위치마다 다른 링크를 쓴다.
유입 경로별 공유 링크. 같은 소개 페이지라도 링크를 붙인 위치마다 다른 링크를 쓴다.

성과 화면에서는 경로별로 방문과 링크 클릭, 손님 활동을 나란히 본다. 손님 활동은 예약, 문의, 전화, 소식 받기처럼 클릭보다 한 걸음 더 나간 행동이다.

방문 경로별로 방문·링크 클릭·손님 활동을 비교한다. 수치는 가렸다.
방문 경로별로 방문·링크 클릭·손님 활동을 비교한다. 수치는 가렸다.

방문을 많이 모은 채널과 실제 연락으로 이어진 채널은 다를 수 있다. 어디에 홍보를 더 할지는 그 차이를 봐야 정할 수 있다.

방문 국가와 지역은 Cloudflare가 요청에 붙여 주는 값을 쓰고, 지역은 국내만 시·도 단위로 남긴다.

방문 국가 지도. 방문이 있었던 대한민국과 미국을 표시했다. 수치는 가렸다.
방문 국가 지도. 방문이 있었던 대한민국과 미국을 표시했다. 수치는 가렸다.

기기는 Client Hints를 먼저 보고, 없으면 User-Agent로 휴대폰·컴퓨터·태블릿을 나눈다.

기기별 방문과 국내 시·도 분포, 손님이 오는 요일·시간대. 수치는 가렸다.
기기별 방문과 국내 시·도 분포, 손님이 오는 요일·시간대. 수치는 가렸다.

해외 방문이 보이면 그 언어의 소개가 필요한지, 휴대폰 방문이 많다면 작은 화면에서 신청 양식이 편한지 살핀다. 손님이 몰리는 시간대는 다음 게시 시점을 정할 때 참고한다.

보인 블록과 눌린 블록

클릭이 적다는 사실만으로는 무엇을 고칠지 알기 어렵다. 그래서 블록마다 보인 수와 누른 수를 따로 센다.

블록별 보인 수와 누른 수, 누른 비율. 수치는 가렸다.
블록별 보인 수와 누른 수, 누른 비율. 수치는 가렸다.

예약 버튼까지 내려온 사람이 적다면 위치를 바꾸고, 충분히 보였는데도 누르지 않았다면 문구를 고친다.

이렇게 기록하느라 방문자를 기다리게 해서는 안 됐다. Worker는 먼저 응답하고 이벤트는 waitUntil로 뒤에서 넘기며, API는 이를 모아 10초마다 한꺼번에 저장한다. 서버가 멈추면 그 10초를 잃을 수 있지만 감수하기로 했다.

페이지 밖으로 이어지는 흐름

다른 링크 모음 서비스와 가장 다른 점은 소개 페이지가 모여의 게시·댓글 기능과 이어져 있다는 것이다. 게시물에 소개 페이지 링크를 넣을 수 있고, 게시한 글마다 공유 링크가 따로 생겨 어떤 글이 방문을 데려왔는지 “게시물에서 온 손님”에서 보인다. Instagram 댓글에 정해 둔 키워드가 달리면 소개 페이지 링크를 DM으로 보낸다.

신청과 소식 메일은 손님의 정보를 받는 곳이라 더 조심했다. 예약과 문의 신청의 개인정보는 건마다 암호화하고, 소식 메일 구독은 확인 메일을 한 번 더 거친다. 이미 쓰는 GA4나 메타 픽셀도 연결할 수 있지만, EEA·영국·스위스 방문자에게는 동의한 뒤에만 켠다.

성과 화면에는 다음 할 일도 둔다. 최근 14일을 정해 둔 규칙으로 살펴, 검색 유입이 없으면 검색 정보를 채우고 공유 링크 방문이 없으면 경로별 링크를 만들라고 권한다.

소개 페이지 성과의 다음 할 일. 방문이 없을 때도 검색 정보와 공유 링크부터 준비하게 한다.
소개 페이지 성과의 다음 할 일. 방문이 없을 때도 검색 정보와 공유 링크부터 준비하게 한다.

AI가 아닌 규칙 기반 제안이라 왜 이 일을 권하는지 그대로 설명할 수 있다. 숫자를 보여 주는 데서 끝내지 않고, 믿고 다음 행동을 고를 수 있는 숫자를 만들고 싶었다.


시장과 가격

여기까지가 모여가 사용자에게 주려는 것이다. 게시하고, 반응을 보고, 문의까지 받는 흐름을 한곳에 모았다면 이제 누구에게 얼마를 받을지 정해야 했다.

초기 스타트업에서 일하며 시장 규모와 고객 확보에 관한 이야기를 자주 들었다. 직접 서비스를 만들고 나니 누구에게 먼저 다가갈지 정하는 데도 필요한 질문이었다. SNS를 쓰는 사람 중에서도 운영에 어려움을 겪고, 그 문제를 해결하려고 도구에 돈을 낼 고객을 좁혀야 했다.

TAM, SAM, SOM은 이 범위를 나누는 데 도움이 됐다.

  • TAM, 전체 잠재시장: 국내외에서 SNS 운영을 위해 도구에 비용을 지불할 수 있는 전체 수요다.
  • SAM, 현재 제공 가능한 시장: 한국어와 현재 지원 채널, 결제·고객 지원 범위 안에서 실제로 도울 수 있는 고객이다.
  • SOM, 초기 확보를 목표로 하는 시장: 내가 도달하고, 사용을 도와주고, 계속 의견을 들을 수 있는 고객이다.

지금은 국내의 1인 사업자와 작은 팀에 집중한다. 업무 이야기를 듣고 첫 사용을 돕기 좋기 때문이다. 초기 고객 수를 정할 때는 혼자서 얼마나 지원할 수 있는지도 고려해야 한다.

시장을 금액으로 추정하려면 업종별로 대상 고객이 몇 곳인지, 얼마나 지불할 의향이 있는지 확인해야 한다. 대상 고객 수에 예상 연간 이용 금액을 곱해 시장을 추정하는 방식이다. 초기 확보 목표인 SOM은 실제 유입과 전환, 유지 데이터를 얻으면서 고칠 계획이다.

국내 고객과 해외 사용

해외에서도 사용할 가능성은 열어 두었다. 게시 예약은 지역 날짜와 시각에 Asia/Seoul 같은 IANA 시간대 식별자를 함께 저장하고, 실제 실행할 시점은 협정 세계시인 UTC로 계산한다. 서머타임 때문에 존재하지 않거나 두 번 나타나는 시각도 처리해야 한다.

일정을 보는 시간대와 성과를 집계하는 기준, 구독의 결제 기준도 구분했다. 팀원마다 같은 하루를 다르게 해석하거나, 설정을 바꿨다는 이유로 결제일이 움직이면 운영하기 어렵기 때문이다.

해외 고객을 만나려면 언어와 현지의 업무 방식, 결제와 지원 방식도 검증해야 한다. 지금은 국내 고객에게 집중하되, 시간대처럼 나중에 고치려면 여러 기능을 함께 바꿔야 하는 기준은 일찍 정리하려 했다.

채널 수에 따른 요금

과금 단위는 연결한 SNS 채널로 잡았다. 채널이 늘면 고객이 관리하는 범위와 수집·처리할 일이 함께 늘어나기 때문이다.

요금은 다음과 같다. 부가세를 포함한 금액이며, 유료 플랜은 최소 3채널부터 시작하도록 설계했다.

요금제채널당 월 결제채널당 연 결제3채널 월 결제
Pro5,900원59,000원17,700원
Team9,900원99,000원29,700원

연간 요금은 월 결제 10개월분이고, 체험은 14일이다. Team에는 구성원 관리와 게시 승인처럼 함께 운영할 때 필요한 기능을 넣어 가격 차이를 설명했다.

지금 가격은 10월 7일부터 적용한 세 번째 가격표다. 채널당 월 요금을 Pro는 4,900원에서 5,900원으로, Team은 7,900원에서 9,900원으로 올렸다. Free는 성과를 최근 7일까지만 보게 줄였고, 신청과 링크 DM의 월 한도도 각각 300건과 100건에서 50건으로 낮췄다. 소수의 사용이 원가를 크게 늘리지 않도록 저장 용량과 영상 크기, 댓글 수집 주기에도 요금제별 상한을 새로 두었다.

최소 3채널 조건 역시 검증할 가정이다. 여러 채널을 관리할수록 가치가 커지겠지만, 1~2채널만 쓰는 고객에게는 시작을 막는 조건일 수 있다. 실제 연결한 채널 수와 반복 사용, 요금제 관심, 이탈 이유를 함께 보면서 판단해야 한다.

결제를 열기 전의 요금제 신청

가격을 정했다고 바로 받을 수 있는 건 아니었다. 결제 기능은 다 만들어 두었지만, 카드사 심사가 끝날 때까지 실제 결제는 닫아 두었다. 그렇다고 유료 요금제를 원하는 고객을 그냥 돌려보내고 싶지는 않았다.

그래서 결제를 열지 말지를 서버 환경 변수 하나로 정했다. BILLING_CHECKOUT_ENABLED가 false이면 요금제 변경 창은 결제 대신 요금제 신청을 받는다. 결제를 바로 시작할 수 있는 상태라면 신청은 받지 않는다. 같은 고객 앞에 결제와 신청이라는 두 길이 겹치지 않게 하려는 장치다.

// 설명을 위해 줄인 코드
// billing.fields.ts
BILLING_CHECKOUT_ENABLED: z.stringbool().default(false),

// request-workspace-plan.use-case.ts
const checkout = await checkoutAvailability.forWorkspace({ workspaceId });
if (checkout.status === "available") {
  return { status: "plan_request_not_needed" };
}
return this.#submit(input, parsedInput, plan, now);ts

신청이 들어오면 운영 메일로 알림이 온다. 운영자는 운영 화면에서 신청을 승인하거나 거절한다. 승인하면 결제 없이 정한 날짜까지 해당 요금제의 유료 한도를 열어 준다. 랜딩 페이지 요금제 카드 아래의 “결제는 곧 열려요. 가입한 뒤 워크스페이스에서 신청하면 확인하고 열어 드려요.”라는 안내가 이 상태에서 보이는 문구다. 이 화면은 뒤에서 가입 CTA를 이야기할 때 함께 본다.

웹은 서버가 내려준 요금제 목록의 결제 가능 여부만 보고 화면을 고른다. 심사가 끝나면 웹을 다시 배포하지 않고 이 값 하나만 바꿔 결제를 연다. GA에서도 결제 시작 checkout_started와 요금제 신청 plan_request_submitted를 다른 이벤트로 센다.

매출과 공헌이익

가격을 정하면서는 고객이 늘어날수록 운영 비용을 감당할 수 있는지도 따져 봤다. 매출은 MRR(Monthly Recurring Revenue, 월 반복 매출)과 ARPA(계정당 평균 매출)로 살펴본다. 연간 구독료는 12개월로 나누어 MRR에 반영한다. 한 번에 들어온 결제 금액과 매달 반복되는 매출을 구분하기 위해서다.

매출에서 결제 수수료, AI 호출, 저장과 전송 같은 변동비를 빼면 공헌이익이 남는다. 이 금액으로 서버 기본 요금 같은 고정비를 충당하고, 서비스를 유지하고 개선할 여유를 확보해야 한다.

아래 숫자는 계산 방식을 보여 주기 위한 가정이며, 모여의 실제 매출이나 원가가 아니다. 부가세를 제외하고 고객당 월 매출이 2만 원, 변동비가 4천 원이라고 하면 공헌이익은 1만 6천 원이다. 월 고정비가 20만 원이라면 고객 13곳의 공헌이익으로 이를 충당한다.

물론 고객이 늘면 필요한 서버 사양과 지원 업무도 달라진다. 무료 크레딧이 끝난 뒤의 비용, 고객 응대와 유지보수에 드는 시간도 함께 살펴야 한다. 가격을 한 번 정하는 것으로 끝내지 않고, 실제 사용량과 운영 부담을 보며 조정하려 한다.

CAC는 고객 한 곳을 획득하는 데 드는 비용이고, LTV는 고객 관계가 이어지는 동안 얻을 가치를 추정한 값이다. 아직 결제와 유지 기간의 표본이 충분하지 않은 모여에서 LTV/CAC 비율을 성과처럼 제시하기는 이르다. 먼저 어떤 경로로 들어온 고객이 작업을 마치고, 결제하고, 다음 달에도 남는지 기록해야 한다.

이미지 보관 비용

가격은 저장 정책에도 영향을 줬다. SNS 게시에 사용한 고해상도 원본을 계속 보관하면 사용량과 비용이 쌓인다. 그렇다고 날짜가 지났다는 이유만으로 예약 게시에 필요한 파일을 지우면 사용자가 맡긴 일을 끝내지 못한다.

모여는 Cloudflare R2에 미디어를 저장한다. 게시가 끝났고 더 이상 사용 중인 곳이 없는 원본에는 요금제별 보관 기간을 적용한다. Free는 30일, Pro는 90일, Team은 180일이다. 원본 보관이 끝나더라도 게시 이력에 필요한 미리보기는 별도로 다룬다.

초안과 예약, 재시도 중인 게시물의 이미지와 소개 페이지, 프로필에서 쓰는 이미지는 지우지 않는다. 사용처가 없는 파일은 별도 유예 기간 뒤 정리하며, 삭제 직전에도 사용 여부와 요금제를 다시 확인한다.

외부 API가 게시할 미디어를 읽을 수 있어야 한다는 요구와 모여가 원본을 얼마나 보관할지는 다른 문제다. 전자는 게시를 완료하기 위한 조건이고, 후자는 사용자의 재사용 편의와 저장 비용 사이에서 정한 제품 정책이다. 요금제를 고를 때 보관 기간이 보여야 하고, 오래 남겨야 하는 원본은 따로 보관하도록 안내해야 한다.


지표를 보는 기준

가격과 제공 범위를 정했다고 끝나지 않는다. 실제로 어떻게 쓰는지 보고 그 판단을 고쳐야 했다. 앞서 본 SNS 성과는 고객이 자기 홍보를 고칠 때 보는 숫자다. 여기서 다룰 GA와 운영 지표는 내가 모여를 고치려고 모으는 숫자다.

GA에서는 화면과 시도한 행동을 보고, 서버에서는 실제 업무가 완료됐는지 확인한다. 예약 버튼을 누른 것과 SNS에 게시된 것은 서로 다른 사건이다. 둘을 연결해서 봐야 버튼을 찾지 못한 문제와 게시 처리에 실패한 문제가 구분된다.

GA에서 보는 행동

어느 유입 경로에서 가입에 관심을 갖는지, 채널 연결을 시작한 사람이 완료했는지, 작성한 게시물을 실제로 올렸는지 알고 싶었다. 그 질문에 맞춰 가입, 연결 시작, 작성 화면 진입, 예약 요청, 요금제 관심을 나누어 수집했다.

이벤트 이름도 channel_connect_started, composer_opened, post_schedule_submitted처럼 관측한 행동이 드러나게 붙였다. 이메일이나 게시물 본문은 분석 도구에 보낼 필요가 없다. 유입과 행동을 이해하는 데 필요한 값만 남겼다.

아래는 이 이벤트들을 모아 둔 GA 보고서다. 운영·테스트 트래픽이 섞여 있을 수 있어 고객의 행동과 구분해서 읽는다.

GA 제품 행동 보고서. 9월 13일부터 10월 10일까지 이벤트별 사용자 수와 횟수를 본다. 수치는 가렸다.
GA 제품 행동 보고서. 9월 13일부터 10월 10일까지 이벤트별 사용자 수와 횟수를 본다. 수치는 가렸다.

한 사람이 채널 연결을 여러 번 시도할 수 있으므로 사용자 수와 이벤트 횟수는 구분해서 본다. 연결을 시작한 사용자 수와 서버의 연결 완료 기록을 함께 봐야 어디에서 막혔는지 보인다. GA에 이벤트가 비어 있으면 아무도 쓰지 않았다고 보기 전에 수집 누락과 필터, 대상 경로부터 점검한다.

가입 CTA와 유입경로 설계

제품 안의 행동보다 앞에 있는 것이 가입이다. 모여 랜딩 페이지에는 가입 버튼이 네 곳에 있다. 상단 헤더와 첫 화면, 요금제 카드, 페이지 마지막이다. 어느 버튼이 실제로 가입을 이끄는지 알아야 버튼의 위치와 문구를 고칠 근거가 생긴다.

로그인하지 않은 상태의 랜딩. 헤더·첫 화면·요금제 카드·마지막 영역의 가입 버튼을 표시했다. 요금제 카드 아래에 결제 안내 문구가 보인다.
로그인하지 않은 상태의 랜딩. 헤더·첫 화면·요금제 카드·마지막 영역의 가입 버튼을 표시했다. 요금제 카드 아래에 결제 안내 문구가 보인다.

그래서 가입 버튼 클릭은 signup_cta_clicked 하나로 보내고, 버튼 위치를 placement에 header, hero, pricing, closing 중 하나로 담았다. 같은 버튼이 로그인한 사람에게는 워크스페이스로 가는 길이 된다. 이 이동까지 세면 가입 관심이 부풀려지므로 로그인하지 않은 방문자의 클릭만 보낸다.

// landing-start-button.tsx, 설명을 위해 줄인 코드
onClick={() => {
  // 로그인한 사람의 워크스페이스 이동은 가입 의도로 세지 않는다.
  if (!isAuthenticated) {
    analytics.track({ name: "signup_cta_clicked", placement });
  }
}}tsx

GA4에서 주요 이벤트로 표시한 것은 sign_up 하나다. 이 이벤트는 이메일 가입이 끝났을 때만 보내고, 소셜 로그인 가입까지 포함한 전체 가입 수는 DB에서 센다. 속성을 만들 때 GA가 켜 둔 close_convert_lead와 qualify_lead는 모여가 보내지 않는 이벤트라 껐다. checkout_started나 post_schedule_submitted도 서버가 응답하기 전의 시도라 전환으로 등록하지 않았다.

숫자를 읽기 전에 섞이면 안 되는 트래픽도 걸렀다. 운영자 계정으로 로그인하면 코드가 traffic_type=internal을 함께 보내고, GA4 데이터 필터 Internal Traffic이 이 방문을 뺀다. 필터는 10월 14일부터 사용 중이며 지난 데이터에는 소급되지 않는다.

로그인과 결제에서 돌아오는 도메인은 원치 않는 추천 목록에 넣었다. accounts.google.com, kauth.kakao.com, tosspayments.com, toss.im이다. 로그인이나 결제를 마치고 돌아온 방문이 새 유입처럼 잡히지 않게 하려는 설정이다. Instagram과 Threads도 OAuth를 거치지만 소개 페이지로 들어오는 실제 유입처라 목록에 넣지 않았다.

마지막으로 탐색 분석에 가입 CTA→첫 공개 유입경로를 만들었다. 단계는 signup_cta_clicked → bio_page_created → bio_page_published다. 가입 버튼을 누른 사람이 소개 페이지를 만들고 처음 공개하기까지를 본다. GA 유입경로는 사용자별 첫 도달을 세므로 따로 조건을 걸지 않아도 마지막 단계가 첫 공개가 된다.

sign_up은 단계에 넣지 않았다. 이메일 가입에서만 보내므로, 넣으면 소셜 로그인으로 가입한 사람이 모두 가입 단계에서 이탈한 것처럼 보인다.

GA4 탐색 분석의 가입 CTA→첫 공개 유입경로. 9월 16일부터 10월 13일까지 세 단계를 기기별로 나눠 본다. 수치는 가렸다.
GA4 탐색 분석의 가입 CTA→첫 공개 유입경로. 9월 16일부터 10월 13일까지 세 단계를 기기별로 나눠 본다. 수치는 가렸다.

기기별로 나누면 모바일과 데스크톱 중 어느 쪽에서 어느 단계에 멈추는지 비교된다. 다만 출시 직후라 표본이 아직 작다. 지금은 비율을 결론으로 읽기보다 끊기는 단계를 찾는 데 쓰고, 실제 가입과 공개 수는 서버 기록과 맞춰 본다.

KPI의 분모와 관측 기간

제품과 사업의 KPI는 실제 가입·게시·청구 기록에서 계산하도록 했다. 고객 지표를 계산할 때는 운영자와 테스터가 소유한 워크스페이스를 제외한다. 유료 전환은 운영 환경에서 0원보다 큰 금액을 실제로 결제했을 때로 정했다. 쿠폰으로 유료 기능을 사용한 것과 돈을 낸 고객을 구분하기 위해서다.

알고 싶은 것보는 기준결과를 보고 할 일
첫 가치를 경험하는가가입 후 7일 내 채널 연결과 첫 게시를 마친 고객 워크스페이스 비율연결·첫 게시에서 막힌 과정 확인
실제 결제로 이어지는가가입 후 90일이 지난 고객 중 그 기간 안에 처음 실제 결제한 비율가격·무료 범위·유료 가치 설명 점검
유료 고객이 남는가월초 유료 구독 중 그달 실제 해지된 비율이탈 이유와 사용 패턴 확인
AI를 감당할 원가로 제공하는가실패를 포함한 추정 원가 합계 ÷ 성공 호출 수재시도·한도·모델 선택 점검

가입한 지 하루 된 사람을 90일 전환율의 미전환 고객으로 계산하면 안 된다. 가입 시기가 비슷한 집단인 코호트를 나누고, 각 집단을 얼마나 오래 관찰했는지도 함께 본다. 초기 활성화·전환 목표는 점검을 위한 가정으로 둔다.

운영 화면에는 수치뿐 아니라 처리할 일도 모았다. 아래 화면에서는 신규 사용자와 채널 연결, 첫 게시를 살펴보면서 결제 문제와 요금제 신청, 게시 장애 같은 대기열로 바로 이동한다.

운영 현황. 처리할 대기열과 사용자, 워크스페이스의 첫 연결·첫 게시를 한 화면에서 본다. 수치는 가렸다.
운영 현황. 처리할 대기열과 사용자, 워크스페이스의 첫 연결·첫 게시를 한 화면에서 본다. 수치는 가렸다.

현재 상태를 세는 카드와 선택한 기간의 사건을 세는 카드는 기준이 다르다. 전체 워크스페이스 수와 기간 내 첫 연결·첫 게시 수를 그대로 나누어 전환율을 계산하지는 않는다. 단계별 이탈을 나타내는 퍼널로 읽으려면 비교할 대상과 관찰 기간부터 맞춰야 한다.

첫 작업을 마친 뒤에는 다음 주에도 게시하는지, 소개 페이지의 문의를 처리하는지 살펴보려 한다. 앞서 사용자에게 물었던 반복 사용의 이유를 실제 기록과 함께 확인하기 위해서다.


사용자가 계정을 지킬 수 있도록

유료 기능을 만들면서 계정 공유와 체험 반복도 생각해야 했다. 서버에서 세션을 관리하고 활성 세션을 최대 5개로 제한했다. 한도를 넘으면 가장 오래 사용하지 않은 세션을 정리한다. 사용자는 보안 설정에서 로그인한 기기를 확인하고 개별 세션이나 다른 기기의 세션을 종료할 수 있다.

사용자에게는 자신의 계정에서 무슨 일이 일어났는지 알 방법도 필요했다. 모르는 기기에서 로그인했다면 바로 알아차려야 대응도 빨라진다.

그래서 새 기기 로그인을 감지하면 브라우저와 운영체제, 로그인 시각을 메일로 안내한다. 본인이 로그인하지 않았다면 보안 설정에서 다른 기기를 모두 로그아웃하고 비밀번호를 바꾸도록 했다. 아래는 실제로 받은 안내다.

새 기기 로그인 안내. 로그인한 기기와 시각을 알리고, 보안 설정에서 다른 기기를 로그아웃하도록 연결한다.
새 기기 로그인 안내. 로그인한 기기와 시각을 알리고, 보안 설정에서 다른 기기를 로그아웃하도록 연결한다.

모든 로그인에 같은 메일을 보내지는 않는다. 기존 사용자가 새 기기로 로그인한 것을 감지했을 때 보내고, 새 계정의 첫 로그인은 제외한다. 같은 기기에서 로그인을 다시 시도해도 안내가 중복되지 않게 했다. 메일마다 비용이 조금씩 들어도 사용자가 직접 대응할 수단은 주고 싶었다.

체험과 쿠폰

세션 제한과 무료 체험의 기준은 따로 정했다. 워크스페이스를 만들 때마다 무료 혜택이 새로 생기지 않도록 체험은 계정당 한 번으로 두고, 미결제 상태의 무료 AI 사용량도 계정 단위로 묶었다. 월 30회와 일 10회 한도를 함께 적용한다.

결제를 열기 전에도 초기 사용자가 필요한 기능을 충분히 써 보게 하고 싶었다. 쿠폰으로 추가 채널이나 Pro·Team 기능을 제공하고, 이용 기간과 발급 조건을 정하게 했다.

쿠폰 만들기. 추가 채널이나 Pro·Team 기능 중 제공할 혜택을 고르고 이용 기간을 정한다.
쿠폰 만들기. 추가 채널이나 Pro·Team 기능 중 제공할 혜택을 고르고 이용 기간을 정한다.

쿠폰은 이용 권한을 주는 수단이라 모든 쿠폰이 청구 금액을 할인하지는 않는다. 체험과 쿠폰, 실결제를 나누어 두면 쿠폰으로 제공한 혜택과 실제 매출이 섞이지 않는다.

오류로 실패한 작업은 사용량을 돌려줄 기준이 필요했다. 같은 실패를 되풀이하며 외부 호출 비용만 쌓는 경로도 막아야 했다. 정상 사용자의 불편과 비용 악용을 함께 따졌다. 인증과 보안의 상세한 고민은 서비스 보안을 점검한 글에 따로 정리했다.

팀의 권한과 운영자의 대응

Team에서는 작성, 승인, 결제 관리 권한을 나눈다. 버튼만 숨기면 요청을 직접 보내는 길이 남는다. 그래서 권한과 요금제, 사용량은 서버가 한곳에서 판정하고, 화면은 그 결과를 안내한다.

운영자에게도 고객에게 설명할 통로가 필요하다. 공지는 약관 변경, 서비스 안내, 광고로 목적을 나누고 대상과 발송 시점을 정한다. 광고 수신 동의처럼 발송 목적에 따라 필요한 조건도 같은 흐름에서 다룬다.

공지 작성. 목적과 수신 대상을 정하고, 고객에게 보낼 내용을 미리 확인한다.
공지 작성. 목적과 수신 대상을 정하고, 고객에게 보낼 내용을 미리 확인한다.

약관 변경 공지는 시행일을 게시일로부터 30일 뒤부터만 잡도록 막아 두었다. 그래서 출시 직전에 고친 10월 13일 판 정책 문서는 약관 변경이 아니라 서비스 안내 공지로 회원에게 보내고, 서비스 화면에 배너를 함께 띄웠다.

사용을 멈추는 순간의 의견도 들을 수 있도록 계정 삭제와 구독 해지에 이탈 설문을 두었다. 어떤 이유로 떠났는지 확인하면 다음 인터뷰에서 물어볼 내용도 구체적으로 정할 수 있다.

이탈 설문. 계정 삭제와 구독 해지 이유를 모아 보고, 남겨 준 의견을 확인한다.
이탈 설문. 계정 삭제와 구독 해지 이유를 모아 보고, 남겨 준 의견을 확인한다.

응답이 쌓이면 사용 기록과 함께 읽으며 어떤 불편을 먼저 해결할지 정할 생각이다.


메일 발송에 SES를 선택한 이유

이런 기능을 정하다 보니 메일은 회원가입 때 한 번 보내고 끝나는 수단이 아니었다. 이메일 인증과 비밀번호 재설정, 새 기기 로그인, 결제 결과, 채널 연결 해제처럼 서비스 이용 중 알려야 할 일이 계속 생긴다. 어떤 메일을 보낼지 먼저 정한 뒤 발송 도구를 비교했다.

Resend는 처음 연동하기 편하다. SDK와 React Email 연동, 발송 로그와 웹훅, 반송 처리 같은 기능을 함께 제공한다. 직접 구현하고 운영할 일을 줄여 주므로 충분히 비용을 지불할 만하다.

다만 모여에는 이미 AWS에서 돌아가는 서버와 백그라운드 작업이 있었다. 필요한 메일을 직접 구성하고 발송 결과를 관리할 기반도 있었으므로, 사용량에 따라 비용을 내는 Amazon SES를 함께 검토했다.

무료 한도와 발송량

Resend의 무료 플랜은 월 3,000통, 하루 100통이다. Pro는 월 20달러에 50,000통을 제공하며 일일 발송 통수 제한이 없다. SES의 기본 종량제 발송료는 1,000통당 0.10달러다. Resend 요금표와 SES 요금표를 기준으로 단순 비교하면 다음과 같다.

월 발송량ResendSES 기본 발송료
3,000통무료$0.30
10,000통Pro $20$1
50,000통Pro $20$5

Resend 무료는 하루 100통 이내로 보낼 때의 비교다. SES 금액은 무료 크레딧을 제외한 기본 발송료이며, 데이터·추가 기능·연계 서비스 요금과 세금은 별도다. SES에도 계정과 리전별 발송 할당량이 있으므로 무제한으로 보낼 수 있다는 뜻은 아니다.

무료 범위에 머무르면 Resend의 발송료가 더 낮다. 그래서 사용자 수만으로 비용을 비교하기보다 한 사람이 몇 통을 받는지와 메일이 어느 날 몰리는지를 봤다. 가입과 인증 재요청, 기기 변경, 결제 안내가 겹치면 월간 합계에는 여유가 있어도 하루 한도에는 먼저 닿을 수 있다.

필요한 보안 안내가 무료 한도 때문에 빠져서는 안 된다고 봤다. 제공할 알림을 먼저 정하고, 그 발송량을 감당할 비용과 운영 방식을 골랐다.

낮은 발송료 뒤에 남는 일

SES가 메일을 받아 갔다고 사용자의 받은편지함에 도착한 것은 아니다. 주소가 잘못됐거나 수신자가 신고했다면 그 결과를 받아 다음 발송에 반영해야 한다. 발송 요청을 재시도할 때 같은 메일이 여러 통 가지 않도록 기록도 남겨야 한다.

모여에서는 발송 작업과 상태를 저장하고 백그라운드에서 처리한다. SES의 반송·불만 이벤트는 SNS와 SQS를 거쳐 서버로 받아 기록한다. 이때 SNS는 소셜 채널이 아니라 AWS의 알림 서비스다. 피드백 처리가 밀리거나 반복해서 실패하는 경우도 별도 경보로 확인하도록 했다.

보낼 수 있다고 많이 보낼 일은 아니다. 일반 알림은 묶어서 보내고 수신자당 하루 20통을 상한으로 둔다. 새 기기 로그인 같은 계정 보안 메일은 이 상한에 걸리지 않는다. 꼭 필요한 안내가 다른 알림에 밀려나지 않게 둘을 나눴다.

따라서 50,000통에 5달러라는 값은 발송료 비교일 뿐이다. 반송 처리와 장애 대응, 유지보수에 드는 내 시간까지 포함하면 운영 비용은 더 커진다. 이 부분을 직접 맡을 수 있는지까지 확인한 뒤 SES를 선택했다.

직접 만들 범위 정하기

예전에는 익숙한 도구를 쓰거나 개발 시간을 아끼기 위해 구독료를 지불하는 경우가 많았다. 혼자 모여를 만들면서는 그 선택을 한 번 더 따져 보게 됐다. 제공하는 기능 중 실제로 쓸 것은 무엇이고, 필요한 부분만 구현한다면 얼마나 들까.

AI의 도움으로 구현과 검토에 드는 부담이 줄면서 이런 비교에 시간을 더 쓸 수 있었다. 메일 도구 전체를 다시 만드는 대신, 모여에 필요한 템플릿과 발송 기록, 실패 처리에 범위를 맞췄다. 직접 맡을 부분을 정하고 나니 고객에게 필요한 알림을 제공하면서도 고정비를 줄일 선택지가 생겼다.

직접 만든 코드는 이후에도 관리해야 한다. 그 부담이 외부 도구의 요금보다 커지면 다시 고른다. 익숙한 도구라도 이번 제품에 필요한 기능과 운영 비용을 다시 비교해 보려 했다.


운영 상황을 알아채는 방법

고객에게 안내를 보내는 것과 별개로, 운영자인 나도 서비스에서 일어난 일을 알아야 한다. 누군가 가입했는지, 청구가 완료됐는지, 예약한 게시물이 실패했는지를 대시보드에서 계속 지켜볼 수는 없다. 알림은 그때 필요한 기록을 찾아보게 하는 출발점으로 삼았다.

가입과 자동 청구 알림

신규 가입과 자동 청구 완료는 Discord 웹훅으로 받는다. 웹훅은 서버에서 지정한 채널로 메시지를 보내는 방식이다. 이런 업무 알림을 받기 위해 별도의 봇이나 유료 협업 도구를 추가할 필요는 없었다.

Discord로 받는 신규 가입 알림. 운영·개발 채널을 나누고 가입 방식과 기기 정보를 남긴다. 이름과 이메일, 계정·요청 식별자와 배포 해시는 가렸다.
Discord로 받는 신규 가입 알림. 운영·개발 채널을 나누고 가입 방식과 기기 정보를 남긴다. 이름과 이메일, 계정·요청 식별자와 배포 해시는 가렸다.

운영과 개발 채널은 나눠 두었다. 테스트 중 만든 계정을 실제 가입으로 착각하지 않기 위해서다. 알림에는 가입 방식과 요청 정보로 추정한 브라우저·운영체제, 관련 기록을 찾을 요청 식별자를 담았다. 이름과 이메일은 일부를 가려 보낸다. 가입 이후 실제로 채널을 연결하고 게시했는지는 앞서 본 운영 화면에서 확인한다.

Slack도 익숙한 선택지였지만, 혼자 운영 알림을 받는 용도에 유료 협업 기능까지 필요하지는 않았다. Slack의 무료 플랜은 최근 90일의 메시지를 조회할 수 있고 1년을 넘긴 데이터는 삭제된다. 모여에서는 기존 Discord 채널을 활용하는 쪽을 택했다. 과거 알림을 찾아보기 위해 별도 구독을 추가하지 않아도 된다는 점이 내게는 유용했다.

다만 결제 기록이나 장애 조사 자료를 Discord에만 맡기지는 않는다. 업무 기록은 서버에 남기고 채팅에는 발생 사실과 찾아볼 단서를 보낸다. 답장이 필요한 고객 문의와 요금제 신청, 실행 중 감지한 장애는 운영 메일로 받는다. 알림을 여러 곳에 똑같이 뿌리기보다 받은 뒤 해야 할 일에 맞춰 통로를 나눴다.

요청과 작업을 이어서 보기

가입 알림과 달리 게시 실패는 어디까지 처리됐는지 따라가야 원인이 보인다. 모여의 웹은 Cloudflare Workers에서 제공하고, API와 예약 게시를 처리하는 백그라운드 작업은 AWS에서 실행한다. 데이터는 RDS에, 이미지 원본과 미리보기는 Cloudflare R2에 보관한다. 서버로 들어오는 연결은 Cloudflare Tunnel을 거친다.

Cloudflare에서는 웹 요청을, 서버 로그에서는 업무 처리 경로를 살핀다. Sentry는 예상 밖 오류를 모아 보고, CloudWatch는 자원 상태와 작업 지연을 감시하는 데 쓴다. 알림을 확인한 뒤 이 기록들에서 영향을 받은 작업과 원인을 좁혀 간다.

서버 로그에는 어떤 작업에서 무슨 일이 생겼는지와 원인 코드, 요청·백그라운드 작업을 연결할 식별자, 배포 버전을 남긴다. 게시 실패가 발생하면 같은 요청이 작업 처리 과정에서 어디까지 진행됐는지 따라갈 수 있어야 한다. 토큰과 쿠키, 개인정보, 외부 API 원문을 그대로 보관하지 않는 것도 기준으로 두었다.

Cloudflare 운영 Worker의 요청 로그. 요청 시각과 처리 결과를 살펴보고 서버의 작업 기록과 함께 원인을 좁힌다.
Cloudflare 운영 Worker의 요청 로그. 요청 시각과 처리 결과를 살펴보고 서버의 작업 기록과 함께 원인을 좁힌다.

권한과 쿠폰처럼 누가 무엇을 바꿨는지 남겨야 하는 일은 감사 기록으로 구분했다. 결제나 권한 변경은 성공했는데 기록만 빠지지 않도록 업무 변경과 함께 저장한다. 반대로 운영 로그 전송에 실패했다고 성공한 결제를 다시 실행해서도 안 된다.

Sentry는 오류 수집에 집중하도록 설정하고, 트레이스 샘플링과 브라우저 세션 녹화는 사용하지 않도록 했다. URL 쿼리와 사용자 정보 등을 제거하고, 사용량 한도 초과처럼 예상한 업무 거절은 장애와 나눈다. 같은 문제를 여러 계층에서 중복 보고하면 알림을 읽고 원인을 찾는 시간이 늘어난다.

경보와 첫 대응

혼자 운영할 때는 모든 순간을 지켜볼 수 없다. 그렇다고 모든 실패가 즉시 개입할 일은 아니다. 잠깐의 자원 사용 증가와 고객의 예약 시간이 계속 밀리는 상황을 구분해야 한다.

경보설정 기준먼저 확인할 것
API·Worker 오류각각 5분 동안 5건 이상오류 종류와 배포 버전, 관련 요청·작업
호스트 메모리85% 초과가 5분 구간 3회컨테이너 사용량과 지속 여부
작업 지연가장 오래된 처리 대상의 지연이 60초 초과, 1분 구간 5회Worker와 외부 API 상태, 영향을 받은 예약
RDS 가용 메모리30분 내내 150MiB 미만연결 수와 정렬·임시 테이블, 인스턴스 증설 여부
RDS Swap30분 내내 64MiB 초과메모리 부족 전조인지, 인스턴스 증설 여부
DB 연결 수10분 동안 50개 초과애플리케이션별 연결 수와 누수·폭주 여부
비용월 110달러 예산의 실제 사용액 80%·예측액 100%비용이 늘어난 서비스와 사용량

작업이 많아도 제시간에 처리하고 있다면 당장 장애는 아니다. 반면 예약 시각을 지난 작업이 쌓이면 고객과의 약속을 이미 어기고 있다. 그래서 자원 비율과 함께 업무 지연을 본다.

표에 넣지 않은 경보도 있다. 디스크 사용률과 RDS CPU는 80% 초과가 5분 구간 3회 이어지면 울린다. SES는 반송 5%와 불만 0.1%를 기준으로 발송 대상을 확인하고, 비용에는 서비스별 하루 5달러 이상의 이상 탐지를 둔다.

표의 RDS 메모리 경보 두 줄은 9월 30일 장애 뒤에 더했다. 그날 아침 db.t4g.micro 인스턴스가 메모리 부족으로 재시작됐고, 백그라운드 작업이 DB에 다시 연결하지 못해 수동으로 재시작할 때까지 약 5시간 멈춰 있었다. 장애 전 며칠 동안 Swap이 43MB에서 113MB로 먼저 늘었기 때문에, 가용 메모리보다 앞서 알려 줄 Swap 경보를 함께 걸었다. 인스턴스는 db.t4g.small로 올리고 DB 연결에 10초 간격의 keepalive를 넣었다.

인스턴스를 올리면서 월 예산도 80달러에서 110달러로 올렸다. 실제 비용은 월 82달러 안팎이라, 실제 사용액 80%인 88달러에 닿으면 평소보다 6달러쯤 더 쓰고 있다는 신호가 된다. 평소 지출에는 울리지 않고 이상 지출에만 울리도록 잡은 값이다. 물론 예산 알림이 지출을 자동으로 막아 주지는 않는다.

실행 중 감지한 장애는 처음에 바로 운영 메일로 알린다. 같은 문제가 계속되면 최초 감지로부터 10분, 1시간, 6시간, 24시간이 지난 시점에 다시 알리고, 이후에는 24시간 간격과 시간당 발송 상한을 적용한다. 각 시점이 지난 뒤 수집기가 해당 문제를 다시 감지해야 보낸다. 시간만 지났다고 무조건 발송하지는 않는다.

긴급 경보와 별개로 매일 아침 Sentry와 Cloudflare 관련 알림, 운영 메일을 확인하는 흐름도 두었다. 이때 공급자가 보내는 정기 보고서와 발생 시점의 장애 알림을 함께 살핀다. 실제로 겪은 DB 연결 풀 고갈 같은 사건은 수집과 대응의 빈틈을 돌아볼 계기가 됐다.


반복되는 운영 업무

알림을 받는 통로를 정한 다음에는 받은 일을 놓치지 않고 처리할 방법이 필요했다. Google Workspace에 support@moyeo.io 같은 업무용 주소를 마련하고, 고객 문의와 운영 알림, 비용 자료가 서로 묻히지 않게 정리했다. 혼자 쓰더라도 나중에 다른 사람이 업무를 이어받을 수 있는 형태로 두고 싶었다.

메일 분류와 아침 점검

Gmail 라벨은 고객 문의·지원, 시스템 장애·운영·보안, 재무 영수증으로 나눴다. 같은 AWS 메일도 비용 안내와 장애 경보는 처리할 일이 다르다. 발신 서비스의 이름보다 메일을 읽고 해야 하는 일을 분류 기준으로 삼았다.

Gmail의 업무별 분류. 고객·재무·개발·장애와 보안을 나누고, 메일 내용과 발신자는 가렸다.
Gmail의 업무별 분류. 고객·재무·개발·장애와 보안을 나누고, 메일 내용과 발신자는 가렸다.

필터로 라벨을 붙이고 받은편지함 옆에 업무별 목록을 배치했다. 바로 답할 문의는 처리하고, 후속 작업이 필요하면 할 일과 다시 확인할 시점을 남긴다.

분류 기준을 정한 뒤에는 Claude 자동화를 매일 아침 실행하도록 했다. 새 메일과 운영 알림을 점검하고, 비용 관련 파일은 분류해 Drive에 정리한 뒤 Google Sheets 비용 대장에 기록한다. 첨부 파일을 내려받고 폴더를 찾고 시트로 옮기는 반복을 줄이기 위한 흐름이다.

비용 기록과 증빙

자동화 결과는 요약문으로만 남기지 않는다. 비용 기록에는 근거 자료의 링크를 남기고, 공식 증빙이 부족하거나 대조가 필요한 항목은 확인할 일로 표시한다. 결제 알림 한 통을 저장했다고 모든 검토가 끝난 것은 아니다.

Drive에는 재무 자료 아래에 구독·계정 관리와 세무·비용 증빙을 나누고, 증빙은 2026년 2기 (7~12월)처럼 귀속 기간별로 모았다. 그 안에서 구매증빙, 카드·은행 자료, 홈택스 자료를 구분한다. 신고 준비나 자료 전달이 필요할 때 메일함부터 다시 뒤지지 않기 위해서다.

비용 대장. 거래별 증빙 종류·확보 상태·공제 검토 상태를 함께 관리한다. 거래일·금액·결제수단·내부 링크는 가렸다.
비용 대장. 거래별 증빙 종류·확보 상태·공제 검토 상태를 함께 관리한다. 거래일·금액·결제수단·내부 링크는 가렸다.

비용 대장에는 거래일과 업체, 통화와 금액, 증빙 링크를 함께 둔다. 원화 확인액과 외화 결제액을 구분하고 취소·환불도 대조한다. 같은 거래의 결제 알림과 청구서가 각각 도착해도 두 번 집계하지 않도록 중복 여부를 확인한다.

증빙 확보와 공제 검토도 다른 상태로 관리한다. 공식 영수증이 있는지, 필요한 정보가 빠졌는지, 카드 청구액과 맞는지 확인할 일이 남을 수 있기 때문이다. 기관별 체크에는 저장할 위치와 다음에 확인할 일을 적고, 요약 탭에서는 보완할 항목을 모은다.

기관별 증빙 체크. 저장할 폴더와 다음 확인 사항을 정하고, 월말에 대조할 순서를 남겼다.
기관별 증빙 체크. 저장할 폴더와 다음 확인 사항을 정하고, 월말에 대조할 순서를 남겼다.

요약 탭에서는 빠진 증빙과 검토할 항목을 한눈에 본다. 비용을 기록한 뒤 신고 준비를 마치려면 무엇이 남았는지 확인하는 곳이다.

홈택스 신고 준비를 위한 내부 요약 시트. 빠진 증빙과 공제 검토가 필요한 항목을 모아 본다.
홈택스 신고 준비를 위한 내부 요약 시트. 빠진 증빙과 공제 검토가 필요한 항목을 모아 본다.

아침의 자료 정리와 월말의 비용 대조, 세금 신고 주기는 구분한다. 일반적인 개인 일반사업자의 부가세 확정 신고는 6개월 과세기간을 기준으로 연 2회이며 중간의 예정고지·신고는 별도다. 국세청 안내를 기준으로 자료를 준비하고, 공식 증빙과 홈택스 조회 자료를 대조해 최종 검토한다.

자동화에 맡긴 것은 자료를 모으고 옮기며 빠진 항목을 찾는 일이다. 검토가 필요한 상태를 그대로 남겨 두면, 아침에는 모든 메일을 처음부터 읽는 대신 그날 판단해야 할 항목에 집중하면 된다.

사용자 이야기와 운영 기록, 비용 자료를 함께 읽고 다음 개선을 정한다.
사용자 이야기와 운영 기록, 비용 자료를 함께 읽고 다음 개선을 정한다.


마치며

초기 스타트업에서 일하며 개발 외의 의사결정을 가까이에서 본 경험이 도움이 됐다. 제한된 시간에 무엇부터 할지, 고객의 요청을 어디까지 받아들일지, 비용을 어떻게 볼지에 관한 고민이었다. 직접 운영을 맡으니 당시 들었던 이야기를 내 서비스의 기준으로 옮겨 보게 됐다.

AI와 함께 만들면서 이런 판단에 쓰는 시간이 가장 크게 늘었다. 구현에 드는 부담이 줄어든 만큼 사용자의 이야기를 듣고 지표와 운영 방식을 비교하는 데 시간을 썼다. 기능을 완성한 뒤에도 사람들이 실제로 쓰는지, 계속 제공할 비용을 감당할 수 있는지 살펴보려 했다.

제품을 만들 때도 AI가 앞으로 더 잘할 일을 고려하려 한다. 모여에서는 콘텐츠와 일정, 댓글, 성과를 함께 보며 지난 작업을 이해하도록 구성했다. 이 맥락이 한곳에 있으면 앞으로 AI나 Computer Use로 업무를 도울 때도 여러 화면에서 정보를 다시 모으는 수고가 줄어든다고 본다. 실제 업무에서 도움이 되는지는 더 확인해야 한다.

서비스를 잘 만들어도 알리는 일은 여전히 어렵다. 특히 혼자 일하거나 작은 팀에서 여러 업무를 맡는 사람에게는 홍보를 꾸준히 이어 가는 일이 부담이 된다. 모여로 돕고 싶은 것도 그 과정이다.

내가 생각하는 Product Builder는 고객의 문제를 듣고 해결책을 만든 뒤, 운영 결과를 보고 다음 결정을 내리는 사람이다. 기술적으로 깊이 파고들어야 할 때도 있고 만들 수 있어도 미뤄야 할 기능도 있다. 모여를 혼자 만들며 각 영역에서 어떤 선택을 할지 판단하는 경험을 쌓고 있다.

출시는 했지만 결제를 연 뒤에도 지금 정한 가격과 무료 범위가 실제 업무에 맞을지는 아직 모른다. 그래서 요즘은 첫 게시 뒤 다시 돌아오는 이유를 듣는 데 가장 많은 시간을 쓴다.