@tenqube/visual-reward-react-native
v1.4.6
Published
Tenqube Visual Reward SDK for React Native
Readme
React Native SDK
React Native WebView 기반 콘텐츠 SDK를 앱에 연동합니다.
준비
요구사항
- React Native 0.77+
- Android minSdk 24 / compileSdk 35 / Kotlin Gradle Plugin 2.0.21+
- iOS 15.1+ (RN 0.77+ 프로젝트 하한)
npm
네 패키지를 함께 설치합니다. 최신 버전은 npm에서 확인할 수 있습니다.
npm install \
@tenqube/visual-reward-react-native \
react-native-webview \
react-native-google-mobile-ads \
@react-native-async-storage/async-storageiOS 는 추가로 pod install 을 실행합니다.
cd ios && pod installGMA 버전별 동작
| react-native-google-mobile-ads | 광고 기능 |
| --- | --- |
| 14.7.0 이상 | 정상 동작 |
| 14.7.0 미만 | 광고 비활성 (RN 0.82 이상에서는 빌드 실패) |
| 미설치 | 광고 비활성 (크래시 없음) |
AndroidManifest 설정
네이티브 광고를 사용하는 경우
App ID 만 선언하면 됩니다. 등록 방법은 App ID를 참고하세요.
네이티브 광고를 사용하지 않는 경우
App ID 검사를 건너뛰는 키를 넣습니다. tools:node="remove" 로 노드를 제거해야 합니다. 선언만 지우면
react-native-google-mobile-ads 가 라이브러리 매니페스트에서 채우는 빈 값이 병합돼
Invalid application ID 로 크래시합니다.
<!-- android/app/src/main/AndroidManifest.xml -->
<!-- manifest 태그에 xmlns:tools="http://schemas.android.com/tools" 가 필요합니다 -->
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
tools:node="remove" />
<meta-data
android:name="com.google.android.gms.ads.INTEGRATION_MANAGER"
android:value="webview" />Info.plist 설정
IDFA 수익화에 필요한 키는 광고 사용 여부와 무관하게 넣습니다.
<!-- ios/<프로젝트>/Info.plist -->
<key>NSUserTrackingUsageDescription</key>
<string>맞춤형 광고 제공을 위해 기기 식별자를 사용합니다.</string>네이티브 광고를 사용하는 경우
App ID 만 선언하면 됩니다. 등록 방법은 App ID를 참고하세요.
네이티브 광고를 사용하지 않는 경우
App ID 검사를 건너뛰는 키를 넣습니다. 없으면 앱 시작 시 GADInvalidInitializationException 으로
크래시합니다.
<!-- ios/<프로젝트>/Info.plist -->
<key>GADIntegrationManager</key>
<string>webview</string>사용법
동작 흐름
initialize()가 서버에서 설정을 로드하고, 이후 open()이 리워드 웹뷰를 엽니다.
sequenceDiagram
participant A as Host App
participant B as VisualReward SDK
participant C as VisualReward 서버
participant D as WebView 화면
A->>B: initialize({ appKey, ... })
B->>C: config 요청
C-->>B: config 응답
B-->>A: onSuccess()
A->>B: open({ serviceName, userId, ... })
B->>D: 웹뷰 열기
D->>B: 웹뷰 닫힘
B-->>A: onCloseVisualRewardContainer 마운트
리워드 웹뷰를 띄우는 컨테이너입니다. 앱 루트에 한 번 넣습니다. 없으면 open() 이 OPEN_FAILED 로
실패합니다.
import { VisualRewardContainer } from '@tenqube/visual-reward-react-native';
export default function App() {
return (
<>
<YourNavigator />
<VisualRewardContainer />
</>
);
}디버그 설정
initialize() 전에 호출하세요.
import { VisualReward } from '@tenqube/visual-reward-react-native';
VisualReward.setDebugEnabled(__DEV__);초기화
앱 시작 시 한 번만 호출합니다.
import { useEffect } from 'react';
import { VisualReward } from '@tenqube/visual-reward-react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
export default function App() {
useEffect(() => {
VisualReward.initialize({
appKey: 'app_key', // 발급된 app_key
adTestMode: false, // 테스트 광고 활성화 여부
storage: AsyncStorage, // config 캐시 저장소
timeoutMs: 5000, // 기본값 (ms)
onSuccess: () => { /* SDK 준비 완료 */ },
onFailure: (e) => { /* 초기화 실패 */ },
});
}, []);
return <VisualRewardContainer />;
}::: info
onSuccess/onFailure는 리워드 설정 로드 결과에 대한 콜백입니다. 광고 엔진 초기화는 백그라운드에서
진행되며 실패해도 리워드 기능은 정상 동작합니다(광고만 비활성).
:::
VisualReward.initialize 파라미터
| 파라미터 | 타입 | 필수 | 설명 | 기본값 |
| --- | --- | :---: | --- | --- |
| appKey | string | ✔ | 발급받은 app_key. stage별로 발급 요청이 필요합니다. | |
| adTestMode | boolean | | 테스트 광고 활성화 여부. 광고 엔진 첫 초기화 시점의 값만 적용됩니다. | false |
| storage | AsyncStorageLike | ✔ | config 캐시 저장소. @react-native-async-storage/async-storage 를 그대로 넘기면 됩니다. 없으면 네트워크 실패 시 폴백할 값이 없어 open() 이 열리지 않습니다. | |
| timeoutMs | number | | open() 이 초기화 완료를 기다리는 상한(ms). 초과 시 open() 의 onError 가 INITIALIZE_TIMEOUT 입니다. | 5000 |
| showDefaultErrorUi | boolean | | open() 이 실패했을 때 SDK 가 디폴트 에러 페이지를 띄운 뒤 onError 를 호출합니다. 원격 안내 페이지를 먼저 띄우고, 실패하면 앱에 동봉된 HTML 로 떨어집니다. <VisualRewardContainer /> 가 마운트돼 있어야 합니다. 화면에 표시되는 진단 코드는 디폴트 에러 페이지 code 를 참고하세요. | true |
| onSuccess | () => void | | 초기화 성공 콜백 | |
| onFailure | (error: VisualRewardError) => void | | 초기화 실패 콜백 | |
::: warning 이미 onError 로 안내하고 있다면 showDefaultErrorUi: false 로 끄세요
기본값이 켜져 있어 그대로 두면 안내가 두 번 뜹니다.
:::
웹뷰 열기
SDK 초기화 성공 이후에 호출합니다.
import { VisualReward } from '@tenqube/visual-reward-react-native';
await VisualReward.open({
serviceName: 'anyService', // 서비스 식별자
userId: user.id, // 사용자 식별자
locale: 'ko',
extra: undefined,
onClose: () => { /* 웹뷰 닫힘 */ },
onError: (e) => { /* 에러 처리 */ },
});VisualReward.open 파라미터
| 파라미터 | 타입 | 필수 | 설명 | 기본값 |
| --- | --- | :---: | --- | --- |
| serviceName | string | ✔ | 서비스 식별자. 사용할 값은 별도로 전달드립니다. 빈 문자열이면 서비스 구획 없이 사이트 루트로 진입합니다. | |
| userId | string | ✔ | 사용자 식별자. 비어 있으면 USER_ID_NOT_SET 에러가 발생합니다. | |
| locale | string | | BCP 47 언어 서브태그(ISO 639-1, 2자리 소문자) | "ko" |
| extra | string | | 포스트백 추가 데이터(JSON serialize된 문자열) | |
| onClose | () => void | | 웹뷰가 닫힐 때 호출되는 콜백 | |
| onError | (error: VisualRewardError) => void | | 에러 발생 시 호출되는 콜백 | |
::: info
한 번의 open()은 onClose 또는 onError 중 정확히 하나를 받습니다.
<VisualRewardContainer /> 가 마운트돼 있지 않으면 OPEN_FAILED 로 알립니다. 닫힘 트리거가
여러 번 발생해도 콜백은 한 번만 통보됩니다.
:::
호스트 WebView 브릿지
호스트 앱이 직접 소유한 WebView의 웹 페이지에서 리워드 웹뷰를 열 수 있습니다. 네이티브에서 메시지를 SDK 로 넘긴 뒤, 웹 페이지에서 호출합니다.
네이티브: 메시지 전달
호스트 WebView 의 onMessage 에 SDK 핸들러를 꽂습니다.
import React, { useRef } from 'react';
import WebView from 'react-native-webview';
import { VisualReward } from '@tenqube/visual-reward-react-native';
export function HostWebViewScreen() {
// `useRef<WebView>(null)` 은 react-native-webview 16 미만에서 타입 에러가 납니다.
const webViewRef = useRef<React.ElementRef<typeof WebView>>(null);
return (
<WebView
ref={webViewRef}
source={{ uri: 'https://your-page.example.com' }}
onMessage={VisualReward.hostWebViewOnMessage(webViewRef)}
/>
);
}페이로드 파싱과 오리진 검증은 SDK 가 처리합니다. 같은 WebView 에서 호스트 자체 메시지도 쓴다면
onMessage 를 넘기세요 — 브릿지 메시지가 아닌 것만 그쪽으로 전달됩니다.
onMessage={VisualReward.hostWebViewOnMessage(webViewRef, {
onMessage: (event) => { /* 호스트 메시지 처리 */ },
})}| 파라미터 | 타입 | 필수 | 설명 | 기본값 |
| --- | --- | :---: | --- | --- |
| webView | RefObject<WebView> | ✔ | 호스트 WebView ref. 결과 콜백 주입에 씁니다 | |
| options.onMessage | (event) => void | | 브릿지 메시지가 아닌 onMessage 이벤트를 받습니다 | |
::: details 메시지 처리를 직접 하려면 — handleWebBridgeMessage
onMessage 를 호스트가 완전히 통제해야 하는 경우에 씁니다. 페이로드 파싱과 fncName 필터를 직접
하고, 오리진 검증에 쓸 URL 도 직접 넘겨야 합니다.
onMessage={(event) => {
const payload = JSON.parse(event.nativeEvent.data);
if (payload.fncName !== VisualReward.WEB_BRIDGE_HANDLER) return;
const webView = webViewRef.current;
if (!webView) return;
VisualReward.handleWebBridgeMessage(payload.params ?? {}, event.nativeEvent.url, webView);
}}url 로 잘못된 값(페이지가 보낸 값, source.uri 등)을 넘기면 오리진 검증이 조용히 무력화됩니다.
그래서 hostWebViewOnMessage 를 권장합니다 — 위 네 가지를 SDK 가 처리합니다.
:::
웹: postMessage 호출
window.ReactNativeWebView.postMessage(JSON.stringify({
fncName: 'visualRewardBridge',
params: {
userId: 'u-123', // 필수: 사용자 식별자
serviceName: 'anyService', // 필수: 서비스 식별자
locale: 'ko', // 선택: 기본 "ko"
extra: '', // 선택: 포스트백 추가 데이터(JSON serialize된 문자열)
},
}));| 파라미터 | 타입 | 필수 | 설명 | 기본값 |
| --- | --- | :---: | --- | --- |
| userId | string | ✔ | 사용자 식별자 | |
| serviceName | string | ✔ | 서비스 식별자. 사용할 값은 별도로 전달드립니다. 값이 빈 문자열이면 사이트 루트로 진입합니다(키를 생략하거나 문자열이 아니면 INVALID_PAYLOAD). | |
| locale | string | | BCP 47 언어 서브태그 | "ko" |
| extra | string | | 포스트백 추가 데이터(JSON serialize된 문자열) | |
응답 콜백은 페이지에서 전역 객체로 등록합니다.
window.visualRewardCallback = {
onClose: function () {}, // 리워드 웹뷰 닫힘
onError: function (code, message) {},
};onError의 code는 네이티브 에러 타입에 대응합니다.
| code | 의미 |
| --- | --- |
| NETWORK_ERROR | 설정 조회 네트워크 오류 |
| INITIALIZE_TIMEOUT | open() 이 설정 조회를 timeoutMs 까지 기다렸으나 오지 않음. initialize() 의 실패 콜백으로는 오지 않습니다 |
| INVALID_APP_KEY | 존재하지 않는 app_key |
| SERVER_ERROR | 설정 서버 오류 (그 외 2xx 아닌 응답) |
| PARSE_ERROR | 설정 응답 파싱 실패 |
| NOT_INITIALIZED | 앱이 initialize()를 호출하기 전에 열기 시도 |
| USER_ID_NOT_SET | userId 누락 또는 빈 문자열 |
| OPEN_FAILED | 리워드 화면 진입 실패 |
| INVALID_PAYLOAD | serviceName 누락 (브릿지 자체 검증). userId 누락은 USER_ID_NOT_SET 으로 옵니다. 깨진 JSON 은 호스트의 onMessage 에서 먼저 걸러지므로 이 코드로 오지 않습니다. |
네이티브 광고 설정
네이티브 광고를 서빙하는 데 필요한 설정입니다. App ID는 광고 요청에,
app-ads.txt는 이 앱의 광고 인벤토리를 누가 판매할 수 있는지 선언하는 데 쓰입니다.
둘 다 없으면 광고가 채워지지 않습니다.
App ID
AdMob/Ad Manager App ID가 필요합니다. 없다면 요청해주세요. 양 플랫폼 모두에 선언합니다.
app.json 에 넣는 것을 권장합니다. react-native-google-mobile-ads 가 이 값을 읽어 Android 는
매니페스트 placeholder 를, iOS 는 빌드된 앱의 Info.plist(GADApplicationIdentifier)를 채워줍니다.
양 플랫폼을 한 곳에서 관리할 수 있고, 아래의 매니페스트 병합 충돌도 생기지 않습니다.
// app.json
{
"react-native-google-mobile-ads": {
"android_app_id": "ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy",
"ios_app_id": "ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy"
}
}네이티브 파일에 직접 선언하려면 아래와 같이 합니다.
<!-- android/app/src/main/AndroidManifest.xml -->
<!-- manifest 태그에 xmlns:tools="http://schemas.android.com/tools" 가 필요합니다 -->
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy"
tools:replace="android:value" /><!-- ios/<프로젝트>/Info.plist -->
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy</string>::: warning
Android 는 tools:replace="android:value" 가 필요합니다.
react-native-google-mobile-ads 가 라이브러리 매니페스트에서 같은 meta-data 를 이미 선언하므로
(app.json 에 값이 없으면 빈 값), 값이 다르면 매니페스트 병합이 실패합니다.
Manifest merger failed : Attribute meta-data#com.google.android.gms.ads.APPLICATION_ID@value
value=(ca-app-pub-...) from AndroidManifest.xml
is also present at [:react-native-google-mobile-ads] AndroidManifest.xml value=()또한 iOS 에서 app.json 의 ios_app_id 를 쓰면 그 패키지의 빌드 페이즈가 Info.plist 에 값을
주입하므로, Info.plist 에 직접 적은 값과 둘 중 하나만 쓰세요.
:::
app-ads.txt
앱 스토어 등록정보에 있는 개발자 웹사이트의 도메인 루트에 app-ads.txt 를 아래 내용 그대로
게시해주세요(https://<개발자-웹사이트>/app-ads.txt).
::: warning
이미 app-ads.txt 를 운영 중이라면 기존 줄을 지우지 말고 아래 목록을 덧붙이세요. 중복 줄은 제거해도
됩니다. 목록이 갱신되면 다시 전달드립니다.
:::
::: details app-ads.txt 전체 내용 (127줄) <<< @/public/app-ads.txt :::
문제 해결
에러 타입
onFailure/onError 콜백에서 e.code 로 분기합니다.
| 타입 | 설명 | 전달 경로 |
| --- | --- | --- |
| NETWORK_ERROR | 네트워크 연결 실패 | onFailure / open() 의 onError |
| INITIALIZE_TIMEOUT | open() 이 설정을 timeoutMs 까지 기다렸으나 오지 않음 | open() 의 onError |
| INVALID_APP_KEY | 존재하지 않는 app_key | onFailure / onError |
| SERVER_ERROR | 설정 서버 오류 | onFailure / onError |
| PARSE_ERROR | 설정 응답 파싱 실패 | onFailure / onError |
| NOT_INITIALIZED | initialize() 호출 없이 open() 호출 | onError |
| USER_ID_NOT_SET | open()의 userId가 빈 문자열 | onError |
| OPEN_FAILED | 리워드 화면 진입 실패 (예: <VisualRewardContainer /> 미마운트) | onError |
디폴트 에러 페이지 code
showDefaultErrorUi 가 켜져 있을 때 뜨는 화면의 코드입니다. 문구는 사용자가 취할 수 있는 행동 기준으로
3종이고, 화면 하단의 code 는 원인별로 다릅니다. 사용자가 알려준 코드로 원인을 좁힐 수 있습니다.
| code | 원인 | 안내 문구 |
| --- | --- | --- |
| N01 | open() 의 userId 가 비어 있음 | 필수 정보가 존재하지 않습니다. |
| N02 | open JS 인터페이스 요청 페이로드 오류 | 〃 |
| N03 | 네트워크 오류(DNS·연결 실패), 잘못된 serviceName 혹은 누락, 페이지 로드 실패 | 네트워크에 연결할 수 없습니다. |
| N04 | open() 이 설정 조회를 timeoutMs 안에 받지 못함 | 〃 |
| N05 | initialize() 전 open() 호출 | 일시적인 오류가 발생했습니다. (앱 재시작 안내) |
| N06 | 설정 서버 오류 | 일시적인 오류가 발생했습니다. |
| N07 | 설정 응답 파싱 실패 | 〃 |
| N08 | 존재하지 않는 앱 키 | 〃 |
| N09 | <VisualRewardContainer /> 가 마운트돼 있지 않음 | 〃 |
증상별 점검
::: details 광고가 나오지 않아요
react-native-google-mobile-ads 선언 여부와 버전(14.7.0 이상), 양 플랫폼의 App ID를 확인하세요.
비활성 사유는 VisualReward 로그 태그에 남습니다.
:::
::: details 네이티브 광고만 서빙되지 않아요
호스트가 mobileAds().initialize() 를 직접 호출하고 있는지 확인하세요. 광고 엔진 초기화는 SDK 가
담당합니다.
:::
::: details 앱 시작 시 Invalid application ID 로 크래시해요
App ID 없이 쓰는 구성인데 tools:node="remove" 를 넣지 않은 경우입니다.
AndroidManifest 설정을 참고하세요.
:::
::: details Manifest merger failed 로 Android 빌드가 안 돼요
App ID 를 앱 매니페스트에 직접 선언했다면 tools:replace="android:value" 가 필요합니다.
App ID를 참고하세요.
:::
::: details iOS 빌드가 Pods/fmt 의 consteval 에러로 안 돼요
call to consteval function … is not a constant expression 은 SDK 와 무관한 RN 툴체인 문제입니다.
RN 이 고정한 fmt 버전이 Xcode 26 의 clang 에서 컴파일되지 않습니다(핀은 릴리스마다 다릅니다).
fmt 12.1.0 을 고정한 RN 릴리스로 올리거나, RN 쪽 이슈를 따라가세요.
:::
::: details OPEN_FAILED 에러가 발생해요
앱 루트에 <VisualRewardContainer /> 가 마운트돼 있는지 확인하세요.
:::
::: details NOT_INITIALIZED 에러가 발생해요
initialize() 성공 이후에 open()·브릿지를 호출하세요.
:::
::: details NETWORK_ERROR 에러가 발생해요
설정 조회가 네트워크 단계에서 실패한 경우입니다. VisualReward 로그의 원인을 먼저 확인하세요.
호스트를 찾지 못했다는 오류(Unable to resolve host / cannot find host)면 DNS 조회 실패입니다.
서버 장애가 아니라 기기 쪽 문제이므로 다음을 확인하세요.
- 기기의 비공개 DNS(Private DNS) 설정
- 광고 차단 앱 · VPN 사용 여부 (도메인에
reward가 포함돼 차단 목록에 걸리는 사례가 있습니다) - 사내망 · 학교망 프록시 정책
- 브라우저에서
https://visual.reward.tenqube.com접속 가능 여부
일시적인 실패라면 다음 open() 이 설정을 자동으로 다시 조회하므로 앱 재시작 없이 회복됩니다.
:::
해결되지 않으면 setDebugEnabled(true) 로 로그를 켠 뒤 VisualReward 태그 로그와 함께 문의해주세요.
