[Episode.7] TestFlight 기간의 개선과 앱스토어 심사 준비, 통과까지 | 필수 항목 체크·Vercel 프록시 보안 정비·스토어 자산

[Episode.7] TestFlight 기간의 개선과 앱스토어 심사 준비, 통과까지 | 필수 항목 체크·Vercel 프록시 보안 정비·스토어 자산

지난 이야기

지난 에피소드에서의 과정을 거쳐 ZIP 백업과 Hive 디버깅으로 데이터가 사라지지 않도록 작업했다. 이제 출시를 앞두고 Episode 07에는 TestFlight 기간 동안 발견한 이슈를 수정하고 앱스토어 심사 제출 전 체크리스트와 보안 전략을 정비한 기록을 적었다.

기능 개발도, App Store 행정·정책 절차도 모두 처음이라 뭐 하나 쉽게 굴러가는 게 없었다. 특히 썸네일·설명·키워드·프라이버시·연령 등급 같은 서류성 정보를 채우는 과정이 의외로 까다로웠다. (출시 소식 전해드렸더니 아는 개발자분도 "개발도 개발이지만 앱스토어 등록·출시가 더 어려웠을 텐데"라며 공감해주셨었다..ㅎㅎ) 그래도 이번에 한 번 해봐서 다음은 '아는 매운맛(?)'이라 처음보다 낫겠지, 생각한다. 이번 글은 그 과정에서 부딪힌 허들과 배운 점을 함께 적어두려 한다.

이 글에서 다루는 내용
  • TestFlight 베타 기간 동안 발견한 이슈와 개선 작업
  • Vercel 프록시로 OpenAI 키를 보호한 구조와 체크포인트
  • 개인정보 문서와 App Store 메타 데이터 정리, 심사 제출부터 출시까지

Phase 0 — TestFlight 베타와 심사 체크리스트 정비

첫 빌드 1.0.0 (1)

데이터 안전망을 갖추고 TestFlight 베타 테스트 기간 동안 "심사 제출 전 챙겨야 할 항목"을 정리했다. 앱 아이콘, 개인정보 처리방침 링크처럼 놓치면 심사에서 바로 거절되는 항목을 정리했고 Info.plist에서는 카메라·사진 접근 사유를 명확히 적고 앱 표시 이름 등을 다시 확인했다. .env.production을 추가해 릴리즈 빌드 시 자동으로 프로덕션 설정을 로드하도록 했다. 이를 통해 OpenAI 모델, 프록시 토큰, 로깅 레벨을 릴리즈용으로 분리해 환경 설정이 섞이지 않도록 했다.

App Store Connect 개발자 계정
앱 스토어 커넥트 개발자 계정은 미리 만들어 두면 편하다. Apple Developer Program의 멤버십 비용은 연간 $99 (약 12만원)로 해마다 갱신해야 한다. 등록 시 일회성으로 $25(약 3만원)만 지불하면 되는 안드로이드에 비해 많이 비싸다..

Phase 1 — 릴리즈 빌드와 실측 지표 확보

체크리스트 정리 후 릴리즈 빌드를 만들어 바이너리 크기를 기록하고, 서로 다른 실기기에 설치해 실제 응답 시간, 앱 용량 등을 측정했다. 토끼굴 언락, 레시피 작성, 챌린지 전환 등 핵심 플로우를 릴리즈 모드로 반복 실행하며 응답 속도와 성공 여부를 확인해 두니, 심사 대응에 필요한 수치를 객관적으로 확보할 수 있었다. Debug 모드에서는 데이터 persistence가 왜곡되는 경험을 이미 했기에 QA 문서에도 "Release 모드 실행 필수"를 명시했다.

Phase 2A — Vercel 프록시 구현과 보안 검증

프록시의 경우 TestFlight에 올리기 전에 구축을 마친 상태였다. 앱은 Vercel URL로만 요청을 보내면서 x-app-token 헤더를 붙이고 프록시가 검증한 뒤 OpenAI를 대신 호출한다. 실제 OpenAI 키는 Vercel 환경변수에서 관리되는 구조이다.

Vercel을 선택한 결정적인 이유

1인 개발 워크플로우에 최적화된 구조라고 판단했기 때문이다. 프로토타입 단계부터 비용 부담 없이 테스트할 수 있다는 점이 매력적이었다. 또한 "Node/TypeScript 파일 + 환경변수 몇 개"만으로 API 엔드포인트를 만들 수 있어 서버나 CI를 따로 세팅할 필요가 없었다. 또한 GitHub에 푸시하면 자동 배포되는 구조도 효율적이었다. 대시보드에서 환경변수를 직관적으로 관리할 수 있어 토큰 노출 사고 같은 긴급 상황에서도 빠른 대응이 가능해 보였다. 여러 모로 장점이 많다고 생각했다.

여기에 Rate Limit 시스템을 추가했다. 레이트 리밋은 일정 시간 내 API 호출 횟수를 제한하는 메커니즘이다. 만약 프록시 토큰이 노출되고 악의적인 사용자가 무한 호출을 시도하는 경우 방어막이 있으면 피해를 최소화할 수 있다. 나는 이 기준을 동일 IP & 시간당 50회로 잡았다.(로그인 인증 기능을 붙이지 않았기에, IP 기준으로) Recipesoup 앱 사용자 패턴을 가정해 만든 RATE_LIMITING.md 문서를 기반으로 정했다. 보통의 사용자는 많아도 하루에 3~5개 레시피를 만들 것이고, 각 레시피당 AI 분석 1회씩이면 하루 평균 5회 정도다. 분 단위당 제한을 고려해봤으나 사용자는 "한 번에 여러 레시피를 입력하는 행동"을 취할 수도 있다. 몰아 작업하더라도 시간당 20회 정도가 피크일 것으로 예상하여 50회는 여유로운 수준으로 보았다.

레이트 리밋 카운트는 Vercel KV에 저장하고 TTL(Time To Live)을 3600초(1시간)로 설정해 시간이 지나면 자동으로 초기화되도록 했다. 시간당 50회를 넘기면 HTTP 429(Too Many Requests)를 반환하며, 앱에서는 요청 한도를 초과했다는 안내 메시지를 띄운다.

Phase 2B — TestFlight 중 토큰 노출 사고와 복구

이처럼 Vercel 프록시는 이미 구현되어 작동 중이었지만 TestFlight 기간에 예상치 못한 문제가 발생했다. 앱스토어 심사를 위해 개인정보처리방침과 앱 지원 페이지를 작성해 GitHub Pages로 올리는 과정에서 OpenAI API 키가 담긴 txt 파일이 커밋되어 버렸다. 이 과정을 처음부터 복기해보면 이러하다. 파일을 수동 선택해 올리면 안전할 것이라 생각하며 GitHub Desktop을 사용 중이었는데, 막상 Claude Code 연동(MCP) 시 현재의 폴더 전체가 통째로 올라갈 수 있다는 사실을 모르고 있었다. .env.gitignore로 막아두었지만 문서 작업용 텍스트 파일은 관리 대상에 포함하지 못했다. 저녁 무렵 OpenAI로부터 "OpenAI API - API key Disabled"라는 제목의 키 노출 안내 메일을 받았다. GitHub를 확인한 결과 해당 파일이 public 레포에 있었다.

처음에는 빌드를 새로 올려야 하나? 싶었는데, 구조를 다시 짚어보니 그럴 필요가 없었다. 클라이언트는 x-app-token만 갖고 있고 실제 OpenAI 키는 Vercel 환경변수에서만 사용하고 있었다. 이제 해야 할 일은 노출된 OpenAI 키를 폐기하고 OpenAI/Vercel 환경변수를 새 키로 교체한 뒤 프록시를 재배포하는 것이었다. TestFlight 빌드를 새로 올리지 않아도 5분 만에 AI 기능이 정상화됐다.

이 사건으로 API 키가 노출되면 어떤 준비와 대응이 필요한지, 내가 놓쳤던 게 무엇이었는지 분명해졌다. key 보안에 늘 주의해야 함은 당연하고, .gitignore 범위와 GitHub 사용 시 업로드 범위를 미리 점검해두어야 한다는 사실을 다시 확인했다. Claude Code로 GitHub MCP를 쓸 때는 폴더 전체를 한 번에 업로드하지 않도록 필요한 파일만 지정하거나, 상황에 따라 수동 커밋을 선택하는 것이 더 안전할 때도 있다는 점을 깨달았다. 노출 사실을 인지하면 즉시 어느 작업·어느 파일에서 문제가 생겼는지 파악하고 원인을 제거한 뒤에 교체·재배포 루틴을 따라야 한다. 나중을 위해 키/토큰 교체 절차를 문서화해 두는 것도 방법일 것이다.

Phase 3 — 문서·심사 자료 업데이트

이후에는 App Store Connect에 사용자에게 제공할 개인정보처리방침, 앱 지원/FAQ 문서 url을 넣었다. 지원 페이지에는 기능 지원 범위·문의 채널·데이터 삭제 절차 등을 명시했고, 개인정보처리방침에는 로컬 저장 우선 원칙과 OpenAI API를 통한 선택적 데이터 활용 범위를 설명했다. 심사관 가이드에는 감정 메모 필수 입력, 토끼굴/챌린지 구조, 오프라인 동작 범위를 정리해 리뷰어가 헤매지 않도록 했다. 문서와 코드가 같은 모습을 바라보는지 마지막으로 교차 검토한 단계다.

Phase 4 — TestFlight QA와 앱스토어 제출 패키지 정리

보안 구조를 정리한 뒤에는 이 기간 활용하기 위해 만든 TESTFLIGHT_BUILD_GUIDE.md문서에 환경변수 설정부터 flutter build ios --release, Xcode Archive, App Store Connect 업로드까지 간단한 단계로 기록했다.

이어서 App Store 커뮤니티 체크리스트를 전수 확인했다. Recipesoup 앱의 경우 로컬 기반으로 로그인이나 서버 동기화도 없어 상대적으로 단순한 구조였기에 주로 "미완성으로 보일 만한 흔적"을 없애는 데 집중했다. 12~15개의 앱스토어 출시 전 체크리스트 중에서 나는 스플래시·비활성 기능 라벨을 다시 훑었다.

앱스토어 심사 거절되는 주요 포인트: 개인정보·권한 설정 불일치, SDK 프라이버시 매니페스트 누락, 로그인·결제·외부 링크 처리 오류, 기능 불안정, 설명 부족 등은 심사 거절의 원인이되니 꼭 챙겨야 한다.

Apple은 "Coming soon/준비중" 문구를 '덜 완성된 기능'으로 판단하는 사례가 많기에 이는 필수로 수정해야 하는 요소였다. 스플래시에 나타나는 당신의 이야기를 "준비중...” 텍스트를 "불러오는 중...”으로 바꾸고, 영상 링크 레시피 추출 요청 시 "기능 준비중" 문구를 "지원되지 않습니다"로 수정했다. '설정'에 더미로 넣어두었던 링크와 이메일 등도 최신 구현에 맞춰 다시 연결했다.

앱스토어 미리보기 스크린샷

스토어 자산도 정리했다. App Store 스크린샷은 Figma에서 디바이스 목업을 직접 세팅하고, 감정 아카이빙·토끼굴·AI 분석·로컬 백업을 강조하는 카피까지 새로 작성했다. 앱 설명에는 AI 활용 등 5가지 레시피 입력 방식, 성장 시스템이나 분석 등 핵심 기능을 포함했다. 부제목·프로모션 텍스트·100자 키워드·검색 태그·카테고리·연령 등급도 다시 적어 누락을 방지했다.


Phase 5 — App Store 정식 심사 제출

App Store 앱 설명·부제목·프로모션 텍스트·100자 키워드·검색 태그·카테고리·연령 등급도 모두 챙겨 필수 항목을 최신 구현에 맞춰 수정했다. 그리고 정식 심사를 제출했다. 2일 뒤에 앱스토어에 정식 배포를 할 수 있었다. 드디어 출시!

앱스토어 심사 제출 메일

심사 제출이 완료되면 메일이 발송된다. 보통 앱 심사 제출 후 50%는 하루 안에, 90%는 이틀만에 리뷰가 완료된다고 적혀있었다. 실제로 첫 심사 제출에는 이틀이 소요되고 이후 빌드부터는 하루만에 승인이 났다. '심사중-심사완료'가 주로 새벽에 이루어졌는데, 궁금해서 찾아보니 내 리뷰어는 미국 샌프란시스코 본사에 있는 것 같았다. 심사자마다 스타일이 달라서 꼼꼼한 리뷰어를 만나면 세세한 부분까지 체크하고, 느슨한 리뷰어를 만나면 최소 요건만 보고 통과시키는 경우도 있다고 한다. 하지만 전체적인 기준은 동일하므로, 기본 요건인 '정책 위반·크래시·개인정보·부정확한 메타데이터' 같은 문제가 있으면 통과할 수 없다.

앱스토어 심사 준비 항목 요약
  • 형식 요건 충족: 스크린샷, 설명, 카테고리 입력
  • 규정 준수 체크: 개인정보 처리, 타사 SDK 선언
  • 심사 통과를 위한 문항 작성: 앱 사용 방식, 계정 필요 여부 등 설명
  • 정책 문서 제출: 프라이버시 정책 URL 등

앱 자체 기능 개발도 쉽지 않았지만 App Store Connect에 썸네일·설명·키워드·프라이버시·연령 등급 같은 서류성 정보를 채우는 과정이 꽤나 까다로웠다. 처음인데다 행정 절차 + 정책 준수 작업까지 혼자 다 하려다보니 어려움이 있었다. 그래도 한 번 사이클을 돌았더니 이제 덜 복잡하게 느껴진다. 이번에 잘 해냈으니 다음 앱 제출은 좀 더 빠르게 해낼 수 있을 것 같다!

Recipesoup 앱스토어 바로가기

마무리하며

이번 작업의 목표는 "심사 전에 필요한 것(문서·기능·보안·스토어 자산)을 챙겨서 되돌아올 일을 막자"였다. 특히 아래 네 가지를 체크하는 식으로 진행했다.

첫째, 문서화이다. 첫 정식 심사 제출이라 체크리스트, 빌드/배포 가이드, 심사 대응 메모를 문서로 만들어 기록해두었다. 덕분에 "이번 빌드에 무엇이 들어 있는지" 적는 게 수월했다.

둘째, 심사 통과 요건 충족이다. Info.plist 권한 이유를 적고, "Coming soon" 같은 문구를 빼고, 개인정보 처리방침/지원 페이지를 최신으로 맞췄다. 스토어에 적은 내용이랑 실제 앱이 다르지 않게 신경 썼다.

셋째, 보안 실수 대비이다. GitHub MCP 주의사항을 정리하고 API 키 노출 시 교체 절차와 로그 확인 순서를 프로세스화했다. API 키 노출 사고는 "문서도 배포 대상"이라는 교훈을 남겼다.

넷째, 프록시와 Rate Limit. OpenAI 호출을 프록시로 감싸고 요청 수를 제한해서, 앱·서버·로그가 한 흐름으로 움직이도록 기본 틀을 잡았다.

결국 제출 준비는 코드·문서·스토어 자산을 맞춰주는 일이라는 걸 알았다. 이 흐름을 만든 덕분에 첫 심사 후 이틀 만에 통과할 수 있었다.


다음 에피소드 예고

Episode 08에서는 출시 이후 운영 핫픽스 작업을 다룬다. Update 공지 정비, Supabase 연동, 어필리에이트 배너 삽입 등을 하며 느낀 점을 풀어볼 예정이다.