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 이벤트가 없는 상태입니다. 가능성:
-
placement 가 잘못된 화면에 연결됨:
getOffer()가 호출되는 화면이 할인 SKU 가 실제 노출되는 화면이 아닌 상태 (예: 호출은 일반 구독 페이지에서, 할인 SKU 는 별도 타임세일 화면에서만 노출).❌ 안티 패턴:
getOffer('time_sale_paywall')호출 후, 사용자가 실제로 보는 화면은subscription_paywallplacement 의 일반 구독 페이지. 같은 사용자에 대한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, ... });
} -
logViewProductItem()호출 자체가 누락: SKU 는 화면에 보이지만 view 이벤트가 발행되지 않음. 해당 페이월 코드에서logViewProductItem()호출 라인이 있는지 검색해 누락 여부를 확인 (원칙 4 코드 예시 참고).
패턴 B: 일부 행만 0%
SDK 반환 SKU 중 일부만 페이월에 못 띄우는 상태. 가능성:
-
하드코딩된 SKU:
getOffer()가 SKUX를 추천했는데 페이월은 고정 SKUY노출.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 이벤트가 매칭됨 -
조건부 view 로깅:
getOffer()결과(객체/null)에 따라 view 로깅이 분기됨. (원칙 4 참고.) -
SKU 네이밍 불일치: 대시보드에 등록한 SKU 와 스토어에 등록한 SKU 가 다름 (예:
.60접미사 누락). 대시보드 SKU 와 App Store / Play Console 의 상품 ID 를 교차 확인.
권장 호출 패턴 (코드 예시)
권장 호출 순서: 프로모션 트리거 조건 충족 → getOffer() → UI 렌더 → logViewProductItem(), 모두 같은 흐름 안에서 수 초 이내.
- React Native
- iOS (Swift)
- Android (Kotlin)
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,
});
}
import MonetaiSDK
// 1. SDK 초기화 — 앱 시작 시 1회 호출 (예: SwiftUI App init / AppDelegate)
@main
struct YourApp: App {
init() {
Task {
try? await MonetaiSDK.shared.initialize(
sdkKey: "YOUR_SDK_KEY",
userId: "USER_ID"
)
}
}
var body: some Scene { WindowGroup { ContentView() } }
}
// 2. 프로모션 트리거 조건 충족 시 호출 (예: 사용자가 타임세일 진입, 특정 액션 후 일정 시간 경과 등)
func openTimeSalePaywall() async {
// 트리거 직후 오퍼 요청.
let offer = try? await MonetaiSDK.shared.getOffer(placement: "time_sale_paywall")
// SKU 선택. nil 일 경우 기존 운영하시던 할인율의 SKU 로 fallback.
let sku = offer?.products.first?.sku ?? BASE_DISCOUNT_SKU
// 스토어 정보 조회 후 페이월 렌더.
let storeProduct = await StoreKit.fetchProduct(sku: sku)
renderPaywall(storeProduct)
// 노출 직후 view 이벤트 발행.
try? await MonetaiSDK.shared.logViewProductItem(
ViewProductItemParams(
placement: "time_sale_paywall",
productId: sku,
price: storeProduct.discountedPrice,
regularPrice: storeProduct.regularPrice,
currencyCode: storeProduct.currencyCode
)
)
}
import com.monetai.sdk.MonetaiSDK
import com.monetai.sdk.models.ViewProductItemParams
// 1. SDK 초기화 — 앱 시작 시 1회 호출 (예: MainActivity.onCreate)
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
MonetaiSDK.shared.initialize(
context = this@MainActivity,
sdkKey = "YOUR_SDK_KEY",
userId = "USER_ID"
) { _, _ -> }
}
}
// 2. 프로모션 트리거 조건 충족 시 호출 (예: 사용자가 타임세일 진입, 특정 액션 후 일정 시간 경과 등)
fun openTimeSalePaywall() {
// 트리거 직후 오퍼 요청.
MonetaiSDK.shared.getOffer(placement = "time_sale_paywall") { offer, _ ->
// SKU 선택. null 일 경우 기존 운영하시던 할인율의 SKU 로 fallback.
val sku = offer?.products?.firstOrNull()?.sku ?: BASE_DISCOUNT_SKU
// 스토어 정보 조회 후 페이월 렌더.
val storeProduct = BillingClient.fetchProduct(sku)
renderPaywall(storeProduct)
// 노출 직후 view 이벤트 발행.
MonetaiSDK.shared.logViewProductItem(
ViewProductItemParams(
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 버전