태그 인앱결제RevenueCatGoogle Play안드로이드 권한구독
전체 글 보기

인앱결제

인앱결제를 붙이는 데 든 시간의 대부분은 코드가 아니었다

인앱결제 배선을 하루에 끝내려다 릴리즈가 세 번 막혔습니다. 막은 것은 전부 두 시스템 사이에서 이름이나 범위가 어긋난 자리였고, 어느 것도 원인을 가리키는 에러를 내지 않았습니다.

2026년 8월 20일 4분

Expo SDK 56 · Android targetSdk 36 기준입니다.

앱에 인앱결제를 붙였습니다. 코드는 이미 대부분 있었고, 남은 것은 스토어와 결제 대행사를 연결하는 일이었습니다. 그 일이 하루를 먹었습니다. 릴리즈가 세 번 막혔는데, 막은 것 중에 코드 문제는 하나도 없었습니다.

인앱결제보다 먼저 걸린 것은 권한 하나였습니다

인앱결제와 상관없는 데서 걸렸습니다. 전날 저는 작은 기능을 하나 넣었습니다. 앨범에서 고른 사진과 영상이 언제 찍혔는지 읽어서 기록의 시간을 자동으로 맞추는 것입니다. 어제 찍은 사진을 오늘 넣어도 어제 자리에 놓이게 하려는 것이었고, 그러려면 원본의 촬영 시각이 필요했습니다.

사진은 파일 안의 EXIF 로 됩니다. 영상에는 그게 없어서 미디어 라이브러리를 읽는 패키지를 하나 추가했습니다. 그 패키지가 매니페스트에 권한 여섯 개를 붙였습니다. 저는 그걸 확인하지 않았습니다.

다음 날 스토어가 배포를 거부했습니다.

사진·동영상 권한을 요청하는 모든 개발자는 앱의 핵심 기능을 스토어에 알려야 합니다.

선언서를 써야 한다는 뜻이었습니다. 그런데 그 양식이 콘솔에 안 보였습니다. 스토어는 업로드된 번들에서 권한을 발견해야 그 항목을 띄우는데, 우리 업로드는 그 전에 거부되고 있었습니다. 닭과 달걀입니다.

트랙을 바꿔봐도 같았습니다. 내부 테스트도, 심사 전송을 끄는 옵션도 막혔습니다. 그리고 마지막으로 확인한 것이 결정적이었습니다. 개발자 API 에는 그 선언을 넣는 엔드포인트가 아예 없습니다. 트랙·번들·등록정보·상품까지는 전부 프로그램으로 되는데, 앱 콘텐츠 선언은 사람이 콘솔에 들어가야 합니다.

선언서를 내고 승인을 받았습니다. 그리고 다시 거부됐습니다.

시스템 사진 선택 도구가 핵심 기능을 제공하기에 기술적으로 충분하지 않은 경우에만 이 권한을 요청할 수 있습니다. 모든 버전 코드에서 삭제하십시오.

이 지적은 맞았습니다. 우리는 이미 시스템 선택 도구를 쓰고 있었고, 그것으로 못 하는 것은 영상의 촬영 시각 하나뿐이었습니다. 그 하나 때문에 앨범 전체 접근을 요구한 셈입니다.

패키지를 걷어냈습니다. 권한 여섯 개가 사라졌고 배포가 통과했습니다. 잃은 것은 영상의 원본 시각 하나이고, 사진은 그대로 됩니다.

되찾는 길도 알아냈는데 그게 더 나은 형태였습니다. 안드로이드의 MediaMetadataRetriever 는 넘겨받은 content URI 하나에서 촬영일자를 읽고, 그 URI 에 대한 접근만으로 됩니다. 네이티브 모듈이 필요하지만 권한이 아예 필요 없어집니다. 처음부터 그 길을 봤다면 릴리즈가 세 번 막히지 않았습니다.

”이 색인은 필요하지 않습니다” 를 잘못 읽었습니다

같은 날 다른 기능에서, 문서 여러 컬렉션을 가로질러 찾는 조회를 하나 만들었습니다. 색인 정의를 넣고 배포했더니 이렇게 나왔습니다.

this index is not necessary, configure using single field index controls

“필요 없다” 로 읽고 그냥 뺐습니다. 배포는 통과했습니다.

며칠 뒤 그 조회가 죽었습니다. 화면에는 “지금은 불러올 수 없어요” 만 떴습니다. 서버에서 같은 조회를 직접 돌려보고서야 원인이 나왔습니다.

FAILED_PRECONDITION: requires a COLLECTION_GROUP_ASC index

그 메시지의 뜻은 “필요 없다” 가 아니라 “복합 색인이 아니라 단일 필드 설정으로 넣어라” 였습니다. 자동으로 만들어지는 단일 필드 색인은 컬렉션 범위뿐이고, 컬렉션 그룹 조회는 따로 열어야 합니다.

에러 메시지가 거짓말을 한 것은 아닙니다. 제가 두 절 중 앞 절만 읽었습니다.

이름이 두 곳에 있으면 반드시 갈립니다

결제 배선의 나머지는 전부 같은 모양이었습니다. 어딘가에 적은 문자열이 다른 어딘가와 정확히 맞아야 하는데, 안 맞아도 아무도 안 알려줍니다.

  • 상품 식별자에 month 또는 year 가 들어 있어야 앱과 서버가 월간·연간을 가릅니다. 그리고 그 식별자는 만든 뒤 못 바꿉니다.
  • 권한 묶음의 이름이 콘솔과 코드에서 글자 하나까지 같아야 합니다. 다르면 결제는 성공하는데 잠금이 안 풀립니다. 돈은 나가고 앱은 그대로입니다.
  • 상품 묶음을 “기본” 으로 지정하지 않으면 목록이 비어 옵니다. 상품도 있고 가격도 있는데 화면에는 아무것도 안 나옵니다.

이 셋 중 어느 것도 예외를 던지지 않습니다. 값이 없거나, 조건이 안 맞아 그냥 지나갑니다.

서비스 계정이 둘인데 이름이 비슷합니다

알림 통로를 만드는 자리에서도 한 번 헤맸습니다. 메시지 큐에 게시 권한을 줘야 하는데, 줘야 할 대상이 스토어 소유의 계정입니다. 내 프로젝트의 계정이 아닙니다.

반대로 내 계정에는 그 큐를 읽을 권한이 필요합니다. 결제 대행사가 내 계정으로 내 프로젝트의 주제 목록을 읽어 자동완성을 채우기 때문입니다. 그게 안 되면 입력칸이 그냥 비어 있고, 왜 비었는지는 안 알려줍니다.

권한을 주기 전에 그 계정으로 직접 찔러본 것이 도움이 됐습니다.

토픽 목록: 403 User not authorized to perform this action

이 한 줄이 “자동완성이 안 뜬다” 보다 훨씬 많은 것을 말합니다.

그래서 무엇을 다르게 하겠는가

절차만 순서대로 필요하시면 전체 체크리스트에 따로 적었습니다.

패키지를 하나 넣을 때 매니페스트를 확인합니다. 작은 기능 하나가 권한 여섯 개를 끌고 들어올 수 있고, 그건 몇 달 뒤 심사에서 돌아옵니다.

두 시스템에 같은 문자열이 들어가면 세는 것을 만듭니다. 저는 앱과 서버의 한도 값을 대조하는 검사를 하나 만들어 뒀는데, 그 뒤로 그 값이 어긋난 적이 없습니다.

막혔을 때 그 조회를 서버에서 직접 돌려봅니다. 클라이언트는 권한·색인·데이터 중 무엇이 없는지 구분해서 말해주지 않습니다. 규칙을 우회하는 자리에서 같은 것을 돌리면 그 셋이 갈립니다.

아직 모르는 것

MediaMetadataRetriever 로 촬영 시각을 읽는 것은 아직 안 만들었습니다. 문서로만 확인했고 실제로 값이 나오는지는 못 봤습니다.

그리고 이 배선이 실제로 돈을 받는지도 아직 모릅니다. 상품과 권한 묶음은 섰지만 결제를 끝까지 해본 적이 없고, 그건 실기기에서 한 번 사보기 전까지는 가정입니다.

이전 글 LLM 호출을 캐시해도 되는지는 온도가 아니라 출력 구조가 정합니다