웹뷰 앱으로 앱 심사 통과하기 (Capacitor)


애플리케이션은 Retention, Session Time, Conversion Rate 등 여러 측면에서 웹 서비스에 비해 더 좋은 모습을 보여준다고 한다.

Ref : https://passion.io/blog/mobile-app-vs-web-app-coaching-business-retention

우리 서비스는 채팅 서비스인 만큼 웹 보다는 앱에서 즐기는 것이 사용자 경험 측면에서 더 자연스럽고, 더 강한 몰입 (Immersiveness) 을 이끌어 낼 수 있을 것이라고 생각하여 앱을 출시하기로 결정하였다.

우리는 기존 웹에서 서비스를 하고 있는 만큼, 웹뷰 기반 앱을 만들기로 결정했는데, 웹 코드를 최대한 재사용하면서 빠르게 앱을 출시하는 것이 목표였다.

다만, 웹뷰 앱은 심사 통과가 훨씬 어렵다고 들어서 최대한 한 번에 심사에 통과할 수 있도록 열심히 준비하였다.

  • 그리고 한 번에 통과 못 하면 신뢰도가 확 떨어져서 Accept 되기 더 어렵다는 소문도 들었다.

이 글은 Capacitor 기반 웹뷰 앱으로 실제 심사를 통과하기 위해 어떤 준비를 했는지를 정리한 기록이다.

각 기능의 세부 구현보다는, 왜 필요했고 무엇을 준비했는지에 초점을 맞췄고, 자세한 구현은 관련 문서를 참고할 수 있도록 링크 위주로 남겼다.


1. Splash Screen

Splash Screen은 앱 실행 시 가장 먼저 보이는 화면이다.
심사 관점에서 보면, 이 화면은 단순한 장식이 아니라 이게 진짜 앱인지를 판단하는 첫 인상에 가깝다.

우리는 기본적인 정적 이미지가 아니라, 브랜드 컬러, 간단한 애니메이션이 포함된 Splash Screen를 구현했다.

이 영역은 웹뷰가 아니라 완전히 네이티브 영역이기 때문에 iOS / Android 네이티브 코드에 직접 Splash 로직을 추가해야 한다.

추가로 한 가지 더 신경 쓴 부분은, Splash Screen이 떠 있는 동안, 백그라운드에서 웹앱 로딩을 함께 진행해서, Splash가 사라진 뒤 최대한 흰 화면이나 로딩 지연 없이 바로 앱 화면이 이어지도록 만들었다.


2. Offline Fallback Page

웹뷰 앱의 가장 큰 약점 중 하나는 인터넷이 끊기면 아무것도 보여줄 수 없다는 점이다.

심사 기준에서도, 네트워크가 없는 상황에서 앱이 아무 반응 없이 멈추거나 크래시 나는 경우를 반드시 테스트 할 것이라고 생각해서 Offline Fallback Page를 준비했다.

앱 시작 시 네트워크 상태를 체크하고 인터넷이 연결되어 있지 않으면 웹뷰 대신 로컬에 포함된 오프라인 페이지를 로드하는 방식으로 구현했다.

이 역시 웹 영역이 아닌 네이티브 앱의 역할이었고, iOS / Android 모두 네이티브 코드로 직접 처리했다.

[iOS 예시 코드]
Swift
/*
 [네트워크 체크]
 - 네트워크 체크 후 네트워크 에러가 감지되면 오프라인 페이지를 로드
*/
private func checkNetworkOnLoad() {
  let monitor = NWPathMonitor()
  monitor.pathUpdateHandler = { [weak self] path in
    if path.status == .satisfied {
    } else {
      log("Network is not available.", level: SentryLevel.warning)
      self?.loadFallbackPage()
    }
    monitor.cancel()
  }
  monitor.start(queue: DispatchQueue.global())
}
[Android 예시 코드]
Java
private void loadOfflinePage() {
  /*
   [네트워크 체크]
   - 네트워크 체크 후 네트워크 에러가 감지되면 오프라인 페이지를 로드
  */
  if (getBridge() != null && getBridge().getWebView() != null) {
    WebView webView = getBridge().getWebView();
    webView.loadUrl("file:///android_asset/offline/index.html");
  }
}
[결과]

3. Push Notification

알림은 네이티브 앱의 핵심 기능 중 하나다.
그리고 앱 심사를 받을 때 중요하게 보는 부분 중 하나는 “이 서비스가 왜 앱이어야 하는가?” 하는 질문에 대한 답이 있어야 한다고 들었다.
그래서 네이티브 Push Notification 기능은 반드시 필요하다고 판단했다.

Firebase Cloud Messaging을 연동해서 네이티브 알림을 표시하고, 알림 클릭 시 특정 링크로 이동하도록 구현하였다.

웹 쪽에서는 @capacitor/push-notifications 플러그인을 사용해서 핸들러를 구현할 수 있다.
우리의 경우에는 Native에서 발급된 Firebase Token을 백엔드로 전달하는 로직과 Notification 클릭 시 특정 링크로 리다이렉트하는 핸들러를 구현했다.

Firebase Token을 백엔드에서 알아야 특정 이벤트가 있을 때 백엔드 API 호출만로 모바일 알림 전송이 가능하다.

빠르게 Notification을 테스트 할 수 있는 Admin Page를 만들어서 Notification을 테스트 할 수 있었다.

[PC / Admin Page 에서 Notification 보내기]
[모바일에서 잘 오는지 확인하기]

Integration은 아래 문서를 참고하면 된다.


4. Deeplink

Deeplink는 심사뿐 아니라, 실제 앱 운영에서도 거의 필수다.
Discord, SNS 등에서 링크를 클릭했을 때 웹사이트가 아니라 앱으로 바로 열리게 만들어야 한다.

Deeplink에는 크기 두 가지 방법이 있다.

  • App Link / Universal Link
    • Web Page 형식
    • https://appDomain.com/chats
  • Custom Scheme
    • appIdentifier://chats

특히 Deeplink는 이후 Appsflyer나 OneLink 같은 툴과 연동할 때도 필수라서 초기에 제대로 잡아두는 것이 중요했다.

Capacitor에서 Deep Link를 지원하기 때문에 아래 문서를 참고하여 Integration을 할 수 있다.


5. Haptics

Haptic은 별 거 아닌 기능이지만 앱이라는 느낌을 주는 데에는 매우 효과적이다.

Capacitor에서는 Haptic 플러그인을 통해 웹에서 간단한 함수 호출만으로 진동을 발생시킬 수 있다.
우리같은 경우는 Footer Navigation Bar 클릭, 채팅 토큰 스트리밍 같은 포인트에 Haptic을 추가했다.

useHaptic 훅을 만들어서 햅틱을 클라이언트에서 쉽게 사용할 수 있도록 하였다.

TypeScript
const haptic = useHaptic()

await haptics.vibrate(duration)
await haptics.impact('light')

아래 문서를 참고하여 Integration을 할 수 있다.


6. Camera / ATT

모바일 앱에서는 카메라 접근, 앱 추적 허용(ATT) 같은 권한 요청이 매우 중요하다.

특히 주의할 점은, 권한 요청 모달에 들어가는 문구를 직접 작성해야 하는데, 왜 이 권한이 필요한지, 어떤 범위로 사용하는지 가 명확하지 않으면 심사에서 리젝될 수 있다.

[ATT]
[Camera]

Integration은 아래 문서에서 확인할 수 있다.

[Capacitor Camera]

[ATT Plugin]


7. Social Login

Apple은 Apple Login 제공을 필수로 요구한다.
또한 네이티브 앱에서는 네이티브 로그인 플로우를 따르는 것이 중요하다.

Capgo Social Login을 사용하면 간편하게 SSO 처리가 가능한데, Native와 Browser API가 통일돼 있어서 아래 함수 호출만으로 쉽게 SSO를 처리할 수 있다.

TypeScript
// Apple Login
const result = await SocialLogin.login({
  provider: 'apple',
  options: {
    scopes: ['email', 'name'],
  },
})

// Google Login
const result = await SocialLogin.login({
  provider: 'google',
  options: {
    scopes: ['email', 'profile'],
    forceRefreshToken: true,
  },
})
[Apple Login]
[Google Login]

Integration은 아래 문서에서 확인할 수 있다.

웹 단에서 처리는 간편하지만, 테스트 때 실제로 기능이 동작해야 하기 때문에 관련 백엔드 로직이 모두 갖추어져 있아야 한다는 점을 명심하자.


8. In-App Purchase (IAP)

인앱 결제는 심사 통과의 가장 큰 관문이었다.
앱에서 유료 기능이 있다면 외부 결제(Stripe, PayPal 등)는 허용되지 않고 반드시 네이티브 IAP를 사용해야 한다.

우리는 RevenueCat을 사용해 iOS / Android 결제를 통합 관리했다.

  • One-time Purchase
  • Subscription Purchase

특히 Subscription은 세일, 무료 체험, 플랜 조합 등 변수가 많아 테스트를 매우 빡세게 해야 한다.

  • Android는 prime-annual:core-prime-free-trial와 같이 :로 나누어 First Period Sale이나 Free Trial을 매우 쉽게 설정할 수 있다.
  • Apple은 상당히 빡세니 주의하자.
    • Introductory Offer는 첫 결제 유저용
    • Promotional Offer는 이미 결제 한 유저용
    • Introductory와 Promotional Offer는 두 개 동시에 적용이 안 된다.
    • 그때는 Offer Codes로 두 개의 Benefits를 동시에 적용해야 한다.
    • 예를 들어, (일주일 Free Trial + First Period Sale)은 Introductory와 Promotional Offer 로는 안 되고, Offer Codes로 해야 한다.
    • 이걸로 대형사고를 쳤기 때문에 나와 같은 실수를 하는 사람은 없었으면 한다…

결제를 클릭하면 아래와 같이 네이티브 결제 창이 떠야 한다.
외부 링크가 IAP 이외의 결제 창이 떠선 안 된다.

추가로 결제는 심사 환경에서도 실제로 동작해야 한다.
Apple의 경우 Storekit으로는 심사 할 수 없다.

Subscription은 실제로 활성화되어야 하고, One-Time Purchase는 실제로 구매한 만큼 수량이 증가해야 한다.
즉, 결제를 처리하는 백엔드 웹훅 처리까지 모두 완료되어 있어야 한다.

Apple의 경우 모든 플랜을 리뷰어가 테스트하지 못 하면 리젝될 수 있다.

  • 실제로 우리 리젝 사유 중 하나였다.

그래서 우리는 세일 플랜이 보이는 계정 / 보이지 않는 계정을 나눠 Comment에 상세히 설명해 모든 플랜을 확인할 수 있도록 했다.


이외에도 서비스 자체의 정책으로

  • 결제 정책 페이지(Payment Terms)로 연결되는 버튼이 결제 플랜 페이지/모달에 보여야 한다.
  • 컨텐츠를 숨기거나 피드백 할 수 있는 시스템이 있어야 한다.
  • 지나치게 선정적인 컨텐츠가 포함되면 안 된다.
    등 부가적인 정책도 만족시켜야 한다.

위 세 개가 앱 심사 전 추가적으로 처리한 내용이고, 나머지는 자연스럽게 포함된 거 같다.


Capacitor 도입부터 앱 심사를 넣는데까지는 2-3주 정도가 소요되었고, 심사 후 최종 출시까지는 5-6일 정도 걸렸다.

리젝은 2-3번 정도 된 걸로 기억하는데, 다행히 그리 큰 이유로 리젝되진 않아서 바로바로 수정 후 재심사를 요청했더니 크게 어렵지 않게 통과할 수 있었다.

기억나는 리젝 사유를 간단하게 적어보았다.

[안드로이드]

  • 컨텐츠에 피드백 할 수 있는 방법이 없음
  • 컨텐츠가 다소 선정적임

[애플]

  • 카메라 정책 문구가 구체적이지 않음
  • 결제 모달에 결제 정책으로 연결되는 페이지 버튼이 없음

리뷰어마다 보는 포인트가 다른 거 같으니 위 사항은 참고 정도로만 보면 좋겠다.
한 번 앱 출시가 되면 그 이후 심사는 빠르게 통과시켜주니 첫 통과가 관건이다.


[Google Play Store]

[App Store]


앱 준비 전에는 웹뷰 앱은 거의 통과가 불가능한 거처럼 말을 하는 사람들이 많아서 두려웠는데, 필수 기능만 잘 준비하니 생각보다 어려움 없이 최종 심사 통과가 가능헸다.

리젝을 몇 번 받긴 했지만, 어떤 부분이 결격 사유인지 매우 자세히 알려주기 때문에 준비만 잘 했다면 어려움 없이 고쳐서 재심사를 넣을 수 있었다.

직접 각 플랫폼에 앱 세팅을 하고 심사도 넣고 리뷰어와 대화 하는 경험 자체가 새롭고 가치있었다.

iOS와 안드로이드 따로 진행해야 하는 기능이 많아서 Integration 과정이 쉽지만은 않았지만, Capacitor 덕분에 다양한 네이티브 기능을 빠르게 합쳐서 최종 심사까지 통과할 수 있었다.

댓글 남기기

Dalmeng's Footprints에서 더 알아보기

지금 구독하여 계속 읽고 전체 아카이브에 액세스하세요.

계속 읽기