본문으로 건너뛰기

SDK 연동 트러블슈팅

대시보드의 설정 → SDK 연동 페이지는 Monetai SDK 연동 상태를 직접 확인할 수 있는 자가 검증 화면입니다. 이 문서는 각 카드의 의미와 흔한 통합 이슈를 진단하는 방법을 설명합니다.


검증 페이지 읽는 법

페이지에는 세 개의 상태 카드가 있습니다.

카드검증 항목
SDK 연결앱에서 initialize() 가 한 번이라도 호출됐는지
이벤트 수신최근 24시간 내, getOffer() (추천 호출) 와 logViewProductItem() (페이월 노출 기록) 두 이벤트가 모두 도착했는지
SDK 동작최근 24시간 내, getOffer() 가 추천한 상품이 실제로 페이월에 노출되었는지

카드별 진단 가이드

🔴 SDK 연결: 연결 안됨

앱에서 initialize() 호출이 한 번도 보고되지 않은 상태입니다.

확인할 점:

  • Monetai SDK 라이브러리가 앱 빌드에 포함되어 있는지
  • initialize() 가 앱 부팅 시 실행되는지
  • sdkKey 가 대시보드에 표시된 값과 일치하는지

🟡 이벤트 수신: 일부 누락 / 🔴 수신 안됨

getOffer()logViewProductItem() 호출 중 하나만 도착하거나, 둘 다 안 들어오는 상태입니다.

누락 이벤트가능성
getOffer() 호출만 도착, logViewProductItem() 누락페이월에서 노출 이벤트 호출 빠짐
logViewProductItem() 만 도착, getOffer() 누락SDK 의 추천 호출이 없음
둘 다 누락SDK 가 아직 연동되지 않았거나, 최근 24시간 동안 사용되지 않은 상태

🟡 SDK 동작: 이슈 N건

getOffer() 로 오퍼를 받은 사용자가 그 SKU 를 페이월에서 보지 못한 케이스입니다. 가장 흔하면서 진단 가치가 큰 신호입니다. 아래 '핵심 5가지 원칙' 으로 통합 패턴을 점검하고, '이슈 N건 행 패턴별 진단' 으로 화면 단위 진단을 진행하세요.


핵심 5가지 원칙

대부분의 통합 버그는 아래 5가지 원칙 중 하나를 위반해서 발생합니다.

1. getOffer() 가 반환한 SKU 를 그대로 페이월 UI 에 사용하기

가장 흔한 버그: 앱이 getOffer() 를 호출해 SKU 추천을 받지만, UI 는 다른 SKU 를 노출하는 경우입니다.

안티 패턴:

const offer = await MonetaiSDK.getOffer('main_paywall');
saveToStore(offer); // 저장만 하고 사용하지 않음
showPaywall(BASE_DISCOUNT_SKU); // UI 는 항상 고정 SKU (예: 기존에 운영하시던 60% 할인 SKU) 사용

올바른 패턴:

// BASE_DISCOUNT_SKU = Monetai 적용 전부터 운영해오시던 기본 할인율의 SKU
// (예: '60% 할인'으로 운영해오셨다면 그 SKU)
const offer = await MonetaiSDK.getOffer('main_paywall');
const sku = offer?.products[0]?.sku ?? BASE_DISCOUNT_SKU;
showPaywall(sku);

2. getOffer() 호출 시점은 페이월 노출 시점 가까이

안티 패턴: 앱 부팅 시 getOffer() 미리 호출 → 결과 캐시 → 한참 뒤 페이월 진입할 때 캐시된 SKU 사용.

이 패턴은 검증 시스템을 세 가지 방식으로 깨뜨립니다.

  • 발생률이 0% 가 됩니다. 검증 로직이 같은 사용자에 대해 getOffer() 호출 이후 그 SKU 를 봤는지를 24시간 내에서 매칭하기 때문입니다.
  • 캐시된 추천이 시간이 지나면서 더 이상 최신이 아닐 수 있습니다. AI 모델은 사용자 행동 패턴 기반으로 추천을 지속적으로 갱신하기 때문입니다.
  • 페이월에 도달하지 못하는 사용자에 대해서도 getOffer() 가 호출되어 호출만 누적됩니다.

올바른 패턴: 프로모션 트리거 조건 충족 → getOffer() 호출 → 즉시 결과를 UI 에 반영 → SKU 가 화면에 보이자마자 logViewProductItem() 호출. 모두 같은 코드 흐름 안에서 수 초 이내에 일어나야 합니다.

// 프로모션 트리거 조건 충족 시점 (예: 타임세일 시작)
async function onTimeSaleTrigger() {
const offer = await MonetaiSDK.getOffer('time_sale_paywall'); // 트리거 직후 호출
const sku = offer?.products[0]?.sku ?? BASE_DISCOUNT_SKU;
showPaywall(sku); // 즉시 UI 렌더
logViewProductItem({ placement: 'time_sale_paywall', productId: sku, ... }); // 노출 직후 호출
}

3. 페이월 화면마다 고유한 placement 부여

placement특정 페이월 화면을 식별하는 값 입니다. 두 개의 다른 페이월 화면(예: 온보딩 페이월 vs 이탈 방지 페이월)이 같은 SKU 를 노출하더라도 placement 값은 서로 달라야 합니다.

여러 화면이 같은 placement 를 공유하면 검증 페이지에서 화면을 구별할 수 없게 되고 화면별 분석이 불가능해집니다.

4. SKU 가 페이월에 노출되면 항상 logViewProductItem() 호출

안티 패턴 1: getOffer() 가 추천을 반환했을 때만 logViewProductItem() 호출.

const offer = await MonetaiSDK.getOffer('time_sale_paywall');
if (offer != null) {
showPaywall(offer.products[0].sku);
logViewProductItem({ placement: 'time_sale_paywall', productId: offer.products[0].sku, ... });
// ↑ null 받은 사용자는 if 분기 진입 자체를 안 해 호출 누락
}

안티 패턴 2: getOffer()null 일 때 페이월 진입 자체를 차단 — null 받은 사용자가 페이월에 도달조차 못 하므로 노출 이벤트도 발생하지 않음.

이러면 getOffer()null 을 반환한 사용자의 노출 이벤트가 조용히 누락돼 A/B 비교가 불가능해집니다. AI 최적화 그룹 vs 기존 가격 그룹의 전환율을 정확히 비교하려면 양쪽 모두 추적되어야 합니다.

올바른 패턴: getOffer() 결과(객체든 null 이든)와 무관하게, 페이월에 SKU 가 보일 때마다 항상 호출.

const offer = await MonetaiSDK.getOffer('time_sale_paywall');
const sku = offer?.products[0]?.sku ?? BASE_DISCOUNT_SKU;
showPaywall(sku);
logViewProductItem({ placement: 'time_sale_paywall', productId: sku, ... }); // null 받은 사용자도 동일하게 호출

5. SDK 초기화 완료 후 getOffer() 호출

initialize() 가 끝나기 전에 getOffer() 를 호출하면 null 이 반환됩니다. 초기화 핸들 (Promise / callback / completion delegate) 을 기다린 뒤에 호출하도록 보장하세요.


"이슈 N건" 행 패턴별 진단

SDK 동작 카드에 이슈가 있을 때, placement 아코디언을 펼쳐서 어떤 행이 0% 인지 확인하세요.

패턴 A: 한 placement 의 모든 행이 0%, getOffer 호출은 발생

SDK 가 호출되긴 하지만 매칭되는 view 이벤트가 없는 상태입니다. 가능성:

  1. placement 가 잘못된 화면에 연결됨: getOffer() 가 호출되는 화면이 할인 SKU 가 실제 노출되는 화면이 아닌 상태 (예: 호출은 일반 구독 페이지에서, 할인 SKU 는 별도 타임세일 화면에서만 노출).

    안티 패턴: getOffer('time_sale_paywall') 호출 후, 사용자가 실제로 보는 화면은 subscription_paywall placement 의 일반 구독 페이지. 같은 사용자에 대한 time_sale_paywall 의 view 이벤트가 어디에도 발생하지 않아 매칭 실패.

    // 일반 구독 페이지 — 이 화면의 placement 는 'subscription_paywall'
    function showRegularSubscriptionPage() {
    await MonetaiSDK.getOffer('time_sale_paywall'); // 검증 대상 placement
    showPaywall(REGULAR_PRICE_SKU);
    logViewProductItem({ placement: 'subscription_paywall', productId: REGULAR_PRICE_SKU, ... });
    // ↑ 이 화면 자체 placement 로 view 발행 → 'time_sale_paywall' 추천과 매칭될 view 가 없음
    }

    올바른 패턴: 할인 SKU 가 실제로 노출되는 화면에서만 getOffer() 호출.

    // 타임세일 바텀시트 — 할인 SKU 가 실제 노출되는 화면
    async function onTimeSaleTrigger() {
    const offer = await MonetaiSDK.getOffer('time_sale_paywall');
    const sku = offer?.products[0]?.sku ?? BASE_DISCOUNT_SKU;
    showPaywall(sku);
    logViewProductItem({ placement: 'time_sale_paywall', productId: sku, ... });
    }
  2. logViewProductItem() 호출 자체가 누락: SKU 는 화면에 보이지만 view 이벤트가 발행되지 않음. 해당 페이월 코드에서 logViewProductItem() 호출 라인이 있는지 검색해 누락 여부를 확인 (원칙 4 코드 예시 참고).

패턴 B: 일부 행만 0%

SDK 반환 SKU 중 일부만 페이월에 못 띄우는 상태. 가능성:

  1. 하드코딩된 SKU: getOffer() 가 SKU X 를 추천했는데 페이월은 고정 SKU Y 노출. Y 만 view 이벤트 발생, X 는 0%. (원칙 1 참고.)

    안티 패턴: getOffer() 결과를 받지만 사용하지 않고 항상 고정 SKU 노출.

    const offer = await MonetaiSDK.getOffer('time_sale_paywall');
    // offer 는 받았지만 무시하고 항상 고정 SKU 사용
    showPaywall(BASE_DISCOUNT_SKU);
    logViewProductItem({ placement: 'time_sale_paywall', productId: BASE_DISCOUNT_SKU, ... });
    // → 추천된 다른 SKU(40%, 50%) 는 view 이벤트가 없어 발생률 0% (60% SKU 만 100%)

    올바른 패턴: getOffer() 가 반환한 SKU 를 그대로 페이월에 노출하고 동일 SKU 로 view 이벤트 발행.

    const offer = await MonetaiSDK.getOffer('time_sale_paywall');
    const sku = offer?.products[0]?.sku ?? BASE_DISCOUNT_SKU;
    showPaywall(sku);
    logViewProductItem({ placement: 'time_sale_paywall', productId: sku, ... });
    // → 추천 SKU 와 view 이벤트가 매칭됨
  2. 조건부 view 로깅: getOffer() 결과(객체/null)에 따라 view 로깅이 분기됨. (원칙 4 참고.)

  3. SKU 네이밍 불일치: 대시보드에 등록한 SKU 와 스토어에 등록한 SKU 가 다름 (예: .60 접미사 누락). 대시보드 SKU 와 App Store / Play Console 의 상품 ID 를 교차 확인.


권장 호출 패턴 (코드 예시)

권장 호출 순서: 프로모션 트리거 조건 충족 → getOffer() → UI 렌더 → logViewProductItem(), 모두 같은 흐름 안에서 수 초 이내.

import MonetaiSDK from '@hayanmind/monetai-react-native';
import { useEffect } from 'react';

// 1. SDK 초기화 — 앱 시작 시 1회 호출 (App.tsx 등의 useEffect 에서)
useEffect(() => {
MonetaiSDK.initialize({
sdkKey: 'YOUR_SDK_KEY',
userId: 'USER_ID',
});
}, []);

// 2. 프로모션 트리거 조건 충족 시 호출 (예: 사용자가 타임세일 진입, 특정 액션 후 일정 시간 경과 등)
async function openTimeSalePaywall() {
// 트리거 직후 오퍼 요청.
const offer = await MonetaiSDK.getOffer('time_sale_paywall');

// SKU 선택. null 일 경우 기존 운영하시던 할인율의 SKU 로 fallback.
const sku = offer?.products[0]?.sku ?? BASE_DISCOUNT_SKU;

// 스토어 정보 조회 후 페이월 렌더.
const storeProduct = await fetchStoreProduct(sku);
renderPaywall(storeProduct);

// 노출 직후 view 이벤트 발행.
await MonetaiSDK.logViewProductItem({
placement: 'time_sale_paywall',
productId: sku,
price: storeProduct.discountedPrice,
regularPrice: storeProduct.regularPrice,
currencyCode: storeProduct.currencyCode,
});
}

📌 null 반환 처리에 대한 안내: getOffer()null 을 반환하더라도 앱은 여전히 기본 SKU 로 페이월을 렌더하고 logViewProductItem() 을 호출해야 합니다. 기본 SKU 를 Monetai 대시보드에 등록해두면, baseline / unknown 그룹에 대해 백엔드가 자동으로 그 SKU 가 담긴 Offer 객체를 반환하므로 클라이언트 측에서 null 분기를 둘 필요가 없습니다.


24시간 윈도우에 대한 안내

검증 페이지의 발생률은 24시간 슬라이딩 윈도우를 사용합니다. 의미하는 바:

  • 트래픽이 적은 SKU 는 24시간 안에 통계적으로 의미 있는 표본이 모이지 않을 수 있습니다. 최근에 등록한 SKU 라면 다음 날 다시 확인해주세요.
  • 방금 시작한 캠페인 (24시간 이내) 은 데이터가 부족할 수 있습니다. 캠페인 시작 후 최소 24시간 경과 후에 결과를 판단해주세요.

그래도 안 풀린다면

위 원칙을 모두 적용했는데도 검증 페이지에 이슈가 남아 있다면, 슬랙 또는 이메일로 문의주세요. 문의 시 다음 정보를 함께 주시면 빠르게 진단 가능합니다.

  • SDK 동작 카드와 펼친 placement 아코디언의 스크린샷 (이슈 행이 보이는 상태)
  • getOffer()logViewProductItem() 이 호출되는 코드 위치 (함수명 / 파일명)
  • 앱 버전 및 SDK 버전