Joyuno's Dev Blog

2026-07-09 (목) 개발일지

dev-blog

오늘의 회고: 사주 출시 블로커 — 백엔드는 완성인데 왜 앱이 안 열리나

오늘은 사주 프로젝트의 출시 직전 블로커 두 가지를 정리했다. 하나는 Apple Sign In 프론트엔드가 아예 없어서 App Store 심사가 불가능했고, 다른 하나는 IAP 결제 검증 코드가 실제 운영 환경에서는 동작하지 않는 미묘한 결함이었다. 두 건 모두 “코드가 있다”는 착각에 속아 넘어가기 쉬운 유형이었다.


Apple Sign In — 백엔드는 실검증인데 프론트에 호출이 0건

증상

사회 로그인 기능 감사를 진행하던 중이었다. Google과 Kakao는 백엔드·프론트 모두 정상 동작하는 걸 확인했는데, Apple 로그인 시 iOS 네이티브 화면이 아예 뜨지 않았다. Flutter 앱에서 Sign in with Apple 버튼을 누르면 브라우저 기반 OAuth 흐름(Google/Kakao용)이 열릴 뿐이었다. App Store 심사 지침 4.8은 “다른 소셜 제공 시 Apple Sign In 필수”를 요구하므로 이 상태로 제출하면 리젝이 확정적이었다.

첫 가설

처음엔 iOS entitlements 파일이 누락됐거나 Xcode Capabilities 설정 문제라 생각했다. 프로젝트 전체에서 .entitlements를 grep했더니 결과가 0건 — “역시 entitlements가 없구나” 싶었다. 하지만 그게 근본 원인이 아니었다.

원인 추적 — 결정적 발견

백엔드 코드부터 거꾸로 추적하기 시작했다. social_auth.py:102-139를 열어보니 Apple JWKS 검증이 완전히 구현되어 있었다. https://appleid.apple.com/auth/keys에서 공개키를 fetch하고, id_token의 issuer와 nonce를 검증하는 코드가 라인 단위로 존재했다. 백엔드는 출시 준비가 끝난 상태였다.

그렇다면 프론트엔드다. Flutter 프로젝트의 pubspec.yaml을 열었다. sign_in_with_apple 패키지가 아예 존재하지 않았다. social_login_service.dart를 읽어보니 googleLogin(), kakaoLogin()만 있고 appleLogin() 함수 자체가 없었다. Flutter 앱 전체에서 SignInWithApple 클래스 호출이 0건이었다.

백엔드는 Apple 로그인을 완벽히 처리할 준비가 되어 있었는데, 프론트엔드에서는 호출조차 없었다.

해결

백엔드는 건드릴 게 없었으므로 프론트엔드에만 집중했다.

영역 변경 전 변경 후
pubspec.yaml sign_in_with_apple 없음 sign_in_with_apple: ^6.1.0 추가
social_login_service.dart appleLogin() 미존재 AppleIDProvider를 통한 auth credential 발급 + id_token 반환
iOS Runner.entitlements 파일 없음 com.apple.developer.applesignin entitlement 추가
Info.plist Apple 관련 CFBundleURLTypes 없음 appleid.apple.com redirect URI 등록

특히 social_login_service.dart에서 Credential 획득 후 idToken을 백엔드로 전송하는 파이프라인이 핵심이었다. 백엔드는 이미 POST /auth/social에서 Apple id_token을 받아 검증하도록 되어 있었으므로, 프론트에서 올바른 토큰만 넘기면 끝이었다.

검증 + 남은 과제

Flutter iOS 시뮬레이터에서 Apple 계정으로 로그인 성공 — 백엔드가 새 사용자를 생성하고 JWT를 반환했다. 단, 실제 Apple Developer Program 계정이 필요한 end-to-end 테스트는 아직 진행하지 못했다. 심사 전에 실제 디바이스에서 테스트 결제와 함께 검증할 예정이다.


IAP 실검증 — 환경변수 하나가 ₩1,900 결제 라인 전체를 무력화

증상

어제 Google Play Console에서 테스트 결제를 진행했는데, 서버 로그에는 verify_purchase 호출이 200을 반환하고 있었지만 실제로는 프리미엄이 활성화되지 않았다. 사용자 입장에서는 “결제됐는데 안 열린다”는 1성 리뷰가 나올 수 있는 치명적인 상태였다.

첫 가설

처음엔 결제 검증 코드 자체에 버그가 있다고 생각했다. iap.py:68-107의 verify_receipt 함수를 정독했다. Apple 영수증 검증은 status=21007일 때 Sandbox로 자동 폴백하는 로직이 깔끔했고, Google Play는 google.auth로 서비스 계정을 불러와 androidpublisher API를 호출하는 구조였다. 논리적으로는 문제가 없어 보였다.

원인 추적 — 로그가 말해준 것

실제 서버 로그를 더 깊이 들여다봤다. Apple 검증 부분에서 APPLE_SHARED_SECRET 환경변수가 None으로 찍히고 있었다. iap.py:78에서 os.getenv("APPLE_SHARED_SECRET")가 빈 문자열을 반환했고, 이 상태에서 verify_receipt가 호출되면 Apple 서버에 검증 요청조차 안 하고 바로 fallback 로직으로 빠졌다. fallback 로직은 단순히 {"status": "ok"}를 반환했지만, 실제로는 프리미엄을 부여하지 않았다.

Google Play 쪽도 마찬가지였다. GOOGLE_PLAY_SERVICE_ACCOUNT_FILE이 설정되지 않아 google.auth.service_account.Credentials.from_service_account_file()이 예외를 던졌는데, 그 예외가 try-except에서 묵살되고 200 OK가 반환되고 있었다.

# 문제의 패턴 (iap.py:243-249)
try:
    result = verify_purchase(receipt_data)
except Exception as e:
    logger.error(f"verify failed: {e}")
    # → 예외를 로그만 남기고 HTTP 200 반환
    # → 클라이언트는 "성공"으로 알고 다음 화면으로 진행

구체적으로 requirements.txt에도 google-auth가 빠져 있어서, Google Play 검증 코드가 임포트조차 안 되는 상태였다.

해결

항목 변경 전 변경 후
.env APPLE_SHARED_SECRET 미설정 App Store Connect 공유 비밀 키 추가
.env GOOGLE_PLAY_SERVICE_ACCOUNT_FILE 미설정 서비스 계정 JSON 경로 추가
requirements.txt google-auth 없음 google-auth>=2.27.0 추가
iap.py 검증 실패 처리 예외 발생 시 200 반환 HTTPException(400) 반환

검증 실패 시 HTTPException을 던지도록 수정했다. 이제 클라이언트는 실패를 명확히 인지하고 재시도 또는 오류 화면을 표시할 수 있다.

검증

Sandbox 환경(Apple)과 테스트 카드(Google Play)로 각각 결제 → 영수증 검증 성공 → 프리미엄 활성화까지 확인했다. 단, APPLE_SHARED_SECRET이 운영 서버 .env에 아직 반영되지 않아서 배포 전에 주입해야 한다. 이 이슈는 배포 체크리스트에 P0로 등록했다.


오늘의 한줄

백엔드가 완벽해도 프론트엔드가 없으면 앱은 출시되지 않는다. 그리고 환경변수 하나가 전체 IPA 결제 라인을 조용히 무력화할 수 있다는 걸 다시 배웠다.