다 만들고도 앱스토어에 제출조차 못 했다 — 개인 앱 출시가 멈추는 이유
- 공유 링크 만들기
- X
- 이메일
- 기타 앱
- 1편 떠오른 생각을 Notion에 박제하고 싶었다 발행
- 2편 다 만들고도 제출조차 못 한 첫 버전 현재 편
- 3편 준비 중독, 그리고 마케팅 공포 발행
- 4편 그래서 나는 또 도망쳤다 — v2 전면 재작성 발행
- 5편 Claude Code로 앱을 다시 짓다 발행
- 6편 앱스토어 2회 반려가 진짜로 말한 것 발행
- 7편 너무 느려서 내가 안 썼다 — 온디바이스 STT 피벗 발행
- 8편 매출 $0에서 다시 정한 우선순위 발행
개인 앱을 앱스토어에 출시하는 일은, 앱을 만드는 것보다 '제출' 버튼을 누르는 게 더 어렵습니다. 1편에서 적은 아이디어를, 저는 정말로 끝까지 만들었습니다. 첫 버전(v1)은 네이티브 iOS 앱이었습니다. 많은 사이드 프로젝트가 멈추는 지점들을 저는 거의 다 통과했습니다. 빌드, 결제 연동, 약관, 아이콘까지요.
그런데 마지막 한 칸이 끝내 비어 있었습니다. 앱스토어 심사 제출. v1은 한 번도 세상에 나오지 못한 채 폐기됐습니다. 이 글은 "거의 다 만든 앱"과 "출시된 앱" 사이에 있는, 단 한 번의 클릭을 못 눌렀던 이야기입니다.
이 이야기를 처음부터 솔직하게 쓰려면, 제가 왜 그 버튼을 못 눌렀는지만 설명해서는 부족합니다. 왜 그 버튼 앞에 도달하기까지 그렇게 오래 걸렸는지, 그 시간 동안 무엇을 만들었고 무엇을 만들지 않았는지를 먼저 짚어야 합니다. v1의 기술 스택부터 시작하겠습니다.
v1의 스택
첫 버전은 정공법이었습니다. 당시 제가 가장 확실하게 이해하고 있던 기술들을 골라 쌓았습니다. 새로운 도전이나 실험이 아니었습니다. 이미 개인 자동화 프로젝트에서 어느 정도 검증한 조합이었습니다. 그만큼 초기 개발 속도는 빨랐습니다.
- 앱: SwiftUI + CoreData (네이티브 iOS)
- 인증·DB·스토리지: Supabase
- 백엔드 로직: Supabase Edge Functions (Deno / TypeScript)
- AI: Google Gemini (음성 STT + 분류 + 구조화를 한 번에)
- 저장: Notion API로 페이지 생성
음성을 녹음하면 서버로 올라가고, Gemini가 받아쓰면서 동시에 분류하고, 그 결과가 Notion에 꽂혔습니다. 1편에서 그리던 그림이 실제로 동작했습니다. 처음으로 시뮬레이터에서 이 흐름 전체가 끊기지 않고 돌아가던 날, 저는 솔직히 기뻤습니다. "된다"는 감각이 있었습니다.
SwiftUI를 선택한 이유는 단순했습니다. UIKit보다 선언적이고, 빠르게 화면을 만들 수 있었습니다. CoreData는 로컬 임시 저장 용도였습니다. 네트워크가 없거나 Supabase에 업로드가 실패했을 때 녹음 파일을 잃지 않기 위한 안전망이었습니다. 실제로 이 안전망이 제대로 작동하는 데는 꽤 많은 시간이 걸렸습니다. 비동기 업로드 큐를 구현하면서 CoreData와 Swift Concurrency(async/await) 사이에서 예상치 못한 스레드 충돌이 여러 차례 발생했습니다.
백엔드를 Supabase Edge Functions으로 구성한 것도 처음에는 합리적인 선택처럼 보였습니다. Deno 런타임 위에서 TypeScript로 작성하고, Supabase 콘솔에서 바로 배포할 수 있었습니다. 하지만 iOS 앱에서 Supabase 인증 토큰을 Edge Function에 전달하고, 거기서 Gemini API를 호출하고, 그 결과를 다시 Notion API로 보내는 연쇄 흐름을 안정적으로 만드는 작업은 생각보다 훨씬 많은 디버깅을 요구했습니다. Edge Function 내부에서 오류가 나도 클라이언트 쪽 Swift 코드에서는 단순히 5xx 에러로만 내려왔기 때문입니다. 에러를 추적하기 위해 Supabase 대시보드의 함수 로그를 열어두고 작업하는 게 일상이 됐습니다.
Gemini를 STT와 분류를 동시에 처리하는 단일 모델로 쓴 것은 v1의 가장 큰 아키텍처 결정이었습니다. 녹음 파일을 멀티파트로 전송하면서 프롬프트에 "이 음성을 텍스트로 변환하고, 내용의 카테고리를 분류하고, 핵심 키워드를 추출해서 JSON으로 반환해라"는 지시를 함께 보냈습니다. 단일 API 호출로 세 가지 작업을 처리하는 방식이었습니다. 이것이 나중에 v2 전면 재작성의 가장 큰 이유 중 하나가 됩니다만, 그 이야기는 4편에서 하겠습니다.
내가 "끝냈다"고 생각한 목록
출시 직전이라고 스스로 믿었던 시점의 체크리스트는 이랬습니다. 객관적으로도 적은 양이 아닙니다. 실제로 이 목록을 완성하기까지 몇 달이 걸렸습니다. 그 시간 동안 저는 매일 조금씩, 때로는 주말에 몰아서, 꾸준히 앞으로 나아갔다고 생각했습니다.
- 코어 기능 동작 (음성 → AI → Notion)
- Xcode 빌드 성공, 번들 ID 확정
- Apple Developer 등록 ($99 결제)
- 개인정보 처리방침·이용약관 작성·호스팅
- 앱 아이콘 제작
- 결제(RevenueCat) 프로덕션 연동 — 구독 월간·연간·라이프타임, 페이월 화면 작동
여기까지 보면 "거의 다 한" 프로젝트처럼 보입니다. 저도 그렇게 느꼈습니다. 결제까지 붙였으니, 이제 제출하고 사람들이 들어와서 카드만 긁으면 되는 줄 알았습니다.
Apple Developer 등록 $99는 사실 작은 금액이 아니었습니다. 사이드 프로젝트에 현금을 쓴다는 게 어색했습니다. 하지만 저는 그 결제를 "이제 진지하게 한다"는 선언으로 받아들였고, 오히려 그 이후로 작업 속도가 빨라졌습니다. 돈을 썼으니 뭔가를 내야 한다는 압박이 생겼습니다. 지금 돌아보면 그 압박이 제가 제출을 피하는 것을 더 합리화하게 만든 측면도 있습니다. "아직 부족한 채로 내면 $99가 낭비되는 거 아닌가"라는 생각이 뒤따랐기 때문입니다.
RevenueCat 연동은 체크리스트 항목 중에서 가장 오래 걸렸습니다. RevenueCat 자체의 복잡성보다는, App Store Connect에서 인앱 구매 상품을 설정하는 과정이 처음이라 낯설었습니다. 월간 구독, 연간 구독, 라이프타임 세 가지 상품을 각각 설정하고, 각각의 가격을 설정하고, RevenueCat 대시보드에서 이 상품들을 오퍼링으로 묶고, iOS 앱에서 RevenueCat SDK를 통해 페이월 화면을 띄우는 것까지 연결하는 데 사흘 정도를 썼습니다. 테스트 계정으로 실제 구독 플로우를 처음으로 완주했을 때, 콘솔에 "Transaction updated to active" 메시지가 찍히는 것을 보면서 제법 뿌듯했습니다.
개인정보 처리방침과 이용약관은 앱스토어 심사 제출 전에 반드시 공개 URL이 있어야 합니다. 저는 GitHub Pages에 정적 HTML 파일로 올렸습니다. 법률 전문가가 아니었기 때문에 오픈소스로 공개된 앱 약관 템플릿을 기반으로 수정했습니다. 이 과정에서도 "법률적으로 충분한가"라는 걱정이 꽤 오래 저를 붙들었습니다. 완벽한 약관은 없다는 걸 알면서도, 뭔가 빠진 게 있으면 심사에서 걸릴 수 있다는 불안이 있었습니다.
앱 아이콘은 Figma로 직접 만들었습니다. 마이크 모양과 Notion 로고를 연상시키는 도형을 조합했습니다. Notion 로고를 직접 쓰면 상표권 문제가 있으니, 비슷한 느낌만 주도록 추상화했습니다. 1024x1024 원본을 만들고 Xcode용으로 리사이징하는 데도 처음에는 시행착오가 있었습니다. 지금 생각하면 아이콘에 쓴 시간이 아깝습니다. 출시도 못 할 앱의 아이콘에 이틀을 썼습니다.
그런데 마지막 한 칸이 비어 있었습니다
목록에 없는 게 하나 있었습니다. 앱스토어 심사 제출. 그리고 그 앞에는 더 큰 빈칸이 있었습니다 — 마케팅과 검증이 통째로 0이었습니다.
제가 한 것과 안 한 것을 나란히 적어보면 그림이 명확해집니다.
| 했다 | 안 했다 |
|---|---|
| 앱 개발·빌드 | 심사 제출 |
| 결제 연동 | 데모 영상 |
| 약관·아이콘 | 랜딩 페이지 |
| Apple Developer 등록 | 커뮤니티 포스팅·사용자 인터뷰 |
왼쪽은 전부 "혼자, 컴퓨터 앞에서, 거절 없이" 할 수 있는 일입니다. 오른쪽은 전부 "평가받아야 하는" 일입니다. 심사 제출조차 오른쪽이었습니다. 제출 버튼을 누르는 순간 제 앱은 처음으로 남의 판단 앞에 서게 되니까요.
검증 질문 셋에 대한 답이 모든 걸 말해줍니다.
- 실제 사용자가 한 명이라도 써봤나? 아니오.
- 커뮤니티에 올려봤나? 아니오.
- 누군가에게 돈 내겠냐고 물어봤나? 아니오.
이 세 가지가 모두 "아니오"라는 사실이 당시에는 그렇게 선명하게 보이지 않았습니다. 저는 "아직 제출 전이니까 사용자가 없는 게 당연하다"고 생각했습니다. 하지만 지금 보면 순서가 잘못됐습니다. 제출 전에 이미 누군가에게 보여줬어야 했습니다. TestFlight로 몇 명에게 보내거나, 개발자 커뮤니티에 스크린샷을 올리거나, 가까운 사람에게 화면을 보여주면서 "이거 쓸 것 같아?"라고 물어봤어야 했습니다. 그 어떤 것도 하지 않았습니다.
데모 영상이 없었다는 것도 나중에 깨달았습니다. 앱스토어에 제출하려면 스크린샷이 필요하고, 동영상 미리보기는 선택 사항이지만 실제로는 전환율에 상당한 영향을 미칩니다. 저는 스크린샷도 준비하지 않았습니다. 시뮬레이터에서 찍은 화면 캡처 몇 장이 전부였습니다. 실 기기에서 실제 사용 흐름을 촬영해서 편집하는 작업은 해본 적 없었고, 생각도 하지 않았습니다.
랜딩 페이지도 없었습니다. 앱스토어에 올라가면 그게 랜딩 페이지 역할을 해주겠지라고 생각했지만, 앱스토어 밖에서 사람들이 처음 앱을 발견할 수 있는 접점이 하나도 없었습니다. Product Hunt에 올릴 준비도, 인디해커에 올릴 준비도, SNS에 공유할 것도 없었습니다. 앱이 출시되면 어디서 어떻게 사람들에게 알릴 것인지에 대한 계획이 0이었습니다.
이 모든 것들이 "없다"는 사실을, 저는 출시가 가까워질수록 더 강하게 인식했습니다. 그리고 역설적으로, 그 인식이 제출을 더 늦추는 이유가 됐습니다. "랜딩 페이지도 없이 내면 안 된다", "데모 영상부터 만들자", "아이콘을 조금 더 다듬자"는 생각들이 꼬리를 물었습니다. 하지만 그중 어느 것도 실제로 완성되지 않았습니다.
'제출'이 왜 그렇게 무서웠나
빌드까지는 전부 제 통제 안에 있습니다. 코드는 제 말을 듣고, 안 되면 고치면 됩니다. Xcode가 오류를 뱉으면 그 오류 메시지를 보고 수정하면 됩니다. 수정이 되면 다시 빌드하면 됩니다. 실패가 있어도 그 실패가 저만 알고 있습니다. 제 맥북 앞에서, 저 혼자, 코드 에디터를 보며 해결하면 됩니다. 외부 세계와 단절된 공간에서 일어나는 일이었습니다.
그런데 제출은 다릅니다. 제출은 결과를 내가 정할 수 없는 첫 행동입니다. 반려당할 수 있고, 통과해도 아무도 안 깔 수 있습니다. Apple 심사팀이 내 앱을 보고 "이건 기준 미달"이라고 판단하면 저는 그 판단을 받아들여야 합니다. 내가 아무리 잘 만들었다고 생각해도, 그 생각은 제출 후에는 제 것이 아닙니다. 심사 결과라는 외부 평가에 종속됩니다.
그래서 저는 제출 버튼 앞에서 멈춰서, 대신 "조금만 더 다듬자"로 돌아갔습니다. 아이콘을 한 번 더 손보고, 카피를 다시 쓰고, 결제 화면을 또 만졌습니다. 전부 안전한 일이었습니다. 전부 거절이 없는 일이었습니다. 전부 제 통제 안에 있는 일이었습니다.
이 패턴의 문제는 "다듬기"가 끝이 없다는 점입니다. 아이콘을 다듬으면 앱 설명이 걸립니다. 앱 설명을 다듬으면 온보딩 화면이 걸립니다. 온보딩을 다듬으면 페이월 카피가 걸립니다. 페이월 카피를 다듬으면 다시 아이콘이 보입니다. 저는 이 루프를 몇 주 동안 반복했습니다. 루프를 돌고 있다는 사실을 알면서도 멈추지 못했습니다. "이번 한 번만 더 고치면 진짜로 제출할 수 있을 것 같다"는 느낌이 매번 들었기 때문입니다.
돌이켜보면 저는 제출 자체보다도 "제출 후 심사 반려"를 더 두려워했던 것 같습니다. 반려는 단순한 기술적 실패가 아닙니다. Apple이 제 앱을 "이건 앱스토어에 올릴 수 없는 수준"이라고 판단하는 것입니다. 그 판단이 두려웠습니다. 수개월을 쏟은 결과물이 한 줄 이메일로 부정될 수 있다는 가능성이, 제출 버튼을 누르지 못하게 만든 핵심이었습니다.
재미있는 건, 실제로 v2를 만들고 나서 제출했을 때 두 번 반려를 받았다는 겁니다. 6편에서 다루지만, 그 반려는 생각보다 구체적이었고 수정 가능했습니다. v1 시절에 그토록 두려워했던 "반려"는 현실에서는 "이 부분을 고쳐야 통과된다"는 피드백이었습니다. v1 때 제출해서 반려를 받았더라면, 오히려 더 빨리 앞으로 나아갈 수 있었을 것입니다. 두려움이 오히려 더 긴 후퇴를 만든 셈이었습니다.
그렇게 v1은 "거의 다 된 채로" 멈춰 있다가, 결국 한 번도 제출되지 못하고 다음 버전 재작성에 자리를 내줬습니다. 제출 버튼을 누르지 못한 채 그냥 두었던 마지막 날, 저는 Xcode 프로젝트 폴더를 아카이브 디렉터리로 옮기고 새 폴더를 만들었습니다. 새 버전을 시작한다는 명목이었지만, 사실은 또 다른 형태의 도망이었습니다. 그 이야기는 4편에서 합니다.
출시는 결승선이 아니라, 출발선에도 못 섰습니다
흔히 "출시가 끝이 아니라 시작"이라고 합니다. 제 경우는 더 앞이었습니다. 저는 출발선(제출)에조차 서지 못했습니다. 만드는 일은 출발선까지 데려다주는 준비운동일 뿐인데, 저는 그 준비운동을 무한히 반복하며 출발을 미뤘습니다.
이 사실을 가장 선명하게 깨달은 건 v1을 포기하고 v2를 시작하기 직전이었습니다. 저는 "왜 제출을 못 했을까"를 정리하다가, 준비운동 목록이 얼마나 길어졌는지를 보았습니다. 코어 기능 이후에도 저는 계속 "이것만 더"를 반복했습니다. 결제 연동은 코어 기능이 아니었습니다. 정식 출시 전 얼리 액세스나 베타 단계에서는 결제 없이도 사용자 피드백을 받을 수 있었습니다. 그런데 저는 결제를 먼저 완성하고 출시하려 했습니다. 준비 순서가 틀렸습니다.
개인 앱 개발에서 흔히 볼 수 있는 패턴입니다. 배포 전에 완벽한 상태를 만들려고 하는 것입니다. 하지만 배포 전에 완벽한 상태가 되는 앱은 없습니다. 앱은 사용자가 쓰면서 완성됩니다. 처음 출시되는 버전은 항상 불완전하고, 그 불완전함을 사용자 피드백으로 채워나가는 게 정상적인 과정입니다. 저는 그 과정을 시작하기 전에 완성을 원했습니다.
더 정확히 표현하자면, 저는 "완성" 자체가 목적이 됐습니다. 이 앱이 실제로 사람들에게 쓰이는 것보다, 완성도 높은 앱을 만들었다는 사실 자체가 저에게 보상이 됐습니다. 결제까지 붙였으니 다 됐다는 느낌, 아이콘이 마음에 드니 만족스럽다는 느낌, 이 느낌들이 진짜 목표(사람들이 쓰는 앱)를 대체했습니다. 어느 순간부터 출시는 이 프로젝트의 목표가 아니라, 부수적인 이벤트처럼 뒤로 밀렸습니다.
(이 시리즈는 실제 개발 기록을 바탕으로 하되, 서비스명과 식별 정보는 익명 처리해 옮기고 있습니다.) 사이드 프로젝트가 완성 직전에 멈추는 이유를 여러 글에서 봤습니다. "진짜로 내면 실패할 수 있기 때문에 내지 않는다"는 해석, "내지 않는 이상 가능성은 열려 있기 때문에 내지 않는다"는 해석이 있었습니다. 저에게 해당하는 건 두 번째였습니다. v1은 제출하지 않는 이상 "잠재적으로 성공할 수 있는 앱"이었습니다. 제출하는 순간 그 앱은 "심사를 통과하거나 반려되는 앱"이 됩니다. 가능성의 공간이 좁아지는 걸 원하지 않았습니다. 그래서 제출하지 않았습니다.
이게 단순한 게으름이었을까요? 아니었습니다. 분명한 심리 패턴이었습니다. 다음 편에서 그 패턴을 세 단계로 해부합니다. 이 시리즈에서 가장 아픈 편입니다. 그리고 동시에, 비슷한 경험을 가진 분들에게 가장 유용할 수 있는 편이기도 합니다.
- 떠오른 생각을 Notion에 박제하고 싶었다 발행
- 다 만들고도 제출조차 못 한 첫 버전 현재 편
- 준비 중독, 그리고 마케팅 공포 발행
- 그래서 나는 또 도망쳤다 — v2 전면 재작성 발행
- Claude Code로 앱을 다시 짓다 발행
- 앱스토어 2회 반려가 진짜로 말한 것 발행
- 너무 느려서 내가 안 썼다 — 온디바이스 STT 피벗 발행
- 매출 $0에서 다시 정한 우선순위 발행
- 공유 링크 만들기
- X
- 이메일
- 기타 앱
댓글
댓글 쓰기