인앱브라우저에서 외부 브랜드몰도 띄웠고, 이제 정말로 캐시백 앱의 핵심 기능을 만들 차례였습니다.
사용자가 인앱브라우저에서 구매를 했을 때 주문을 추적하는 것입니다.
샵백이나 다른 캐시백 앱들은 어떻게 하는 걸까 궁금했습니다. 검색해도 구체적인 구현 방법은 나오지 않았죠.
결국 직접 부딪혀가며 하나씩 해결해야 했습니다.
1. 인앱브라우저와 WebView
제가 서비스하는 앱에서 입점할 브랜드몰로 사용자를 이동시키는 방법은 두 가지였습니다.
외부브라우저(Chrome, Safari 등)로 여는 방법과, 앱 안에서 인앱브라우저(WebView)로 여는 방법입니다.
외부브라우저로 열면 구현은 편하지만, 우리 앱은 그 뒤의 행동을 전혀 추적할 수 없습니다.
사용자가 어떤 페이지까지 갔는지, 실제로 결제를 했는지, 주문번호가 뭔지 전혀 알 수 없습니다.
반면 WebView는 앱 안에 갇혀있는 브라우저라서 통제가 가능합니다
그래서 저는 React Native WebView로 인앱브라우저를 만들고, 그 위에서 주문 추적을 하기로 했습니다.
아래는 WebView에서 사용할 수 있는 기능들입니다.
1. URL 변화 감지
<WebView
onNavigationStateChange={(navState) => {
// 구매 완료 페이지인지 확인 가능
console.log('현재 URL:', navState.url);
}}
/>
2. JavaScript 주입
// 웹페이지에 우리 코드를 삽입할 수 있음
const INJECTED_JAVASCRIPT = ``
3. 웹 → 앱 통신
// 웹에서 앱으로 메시지 보내기
window.ReactNativeWebView.postMessage('구매 완료!');
// 앱에서 받기
<WebView
onMessage={(event) => {
console.log('웹이 보낸 메시지:', event.nativeEvent.data);
}}
/>
2. 캐시백 앱의 핵심 로직
캐시백 서비스가 작동하려면 이 과정을 추적해야 합니다.
- 사용자가 우리 앱에서 브랜드 클릭
- 인앱브라우저로 브랜드몰 접속
- 사용자가 상품 구매
- 구매 완료 감지 -> 주문번호, 금액 파악
- 해당 주문이 구매 확정되면 캐시백 지급
- 환불/취소 시 캐시백 회수
인앱브라우저로 브랜드몰에 접속할 수 있으니, 이제는 사용자의 구매를 감지할 차례입니다.
[클릭 추적]
먼저 해결해야 할 문제는 "이 구매가 우리 앱에서 온 건지" 파악하는 것이었습니다.
그래서 사용자가 앱 내에서 브랜드를 클릭한 경우, 브랜드몰 URL에 우리만의 식별용 ID를 붙여서 보냈습니다.
// 사용자가 브랜드 클릭 시 clickID 난수 생성
const clickId = `click_${uuid()}`;
// 실제 이동할 URL
const targetUrl = `https://brand.com?myapp_click_id=${clickId}&brand_id=123456`;
이 myapp_click_id가 나중에 주문번호와 매칭되는 핵심 키가 됩니다.
[구매 감지]
다음으로는 사용자가 브랜드몰에서 결제를 완료한 것을 앱에서 감지해야 했습니다.
일반적인 경우 구매 완료 시에 식별용 ID가 붙어있다면 우리 서버로 데이터를 보내주면 됩니다.
하지만 그럴 수 없었습니다.
왜냐하면 입점 예정 브랜드 대부분이 개발자가 없기 때문이었죠.
카페24나 네이버 스마트스토어 같은 외주 플랫폼이 브랜드몰에 사용됐습니다.
다행히 많은 쇼핑몰이 구매 완료 페이지 URL에 주문번호를 포함하고 있었습니다.
그래서 URL을 파싱해 식별용 ID와 매칭했습니다.
// 네이버 스마트 스토어 예시(주문번호: 2026010112345678)
https://smartstore.naver.com/ordersheet/pay-result/2026010112345678
// 네이버 스마트스토어 패턴
if (url.includes('/ordersheet/pay-result') || url.includes('/order/result')) {
const parts = url.split('/');
const orderId = parts.pop(); // 마지막 부분이 주문번호
// 서버에 전송
trackConversion({
affiliate_click_id: myappClickId,
order_sync_id: orderId
});
}
일부 쇼핑몰은 SPA 기반이라 페이지가 넘어가도 실제로는 URL이 바뀌지 않습니다.
React나 Vue로 만든 SPA는 history.pushState로 URL을 조작하거든요.
WebView의 onNavigationStateChange는 실제 페이지 이동 위주로 감지하기 때문에, SPA에서는 작동하지 않습니다.
그런 경우 JavaScript를 주입해서 history.pushState를 가로채는 방법을 사용했습니다.
const INJECTED_JAVASCRIPT = `
(function() {
// 원래 함수 저장
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
// 래핑
history.pushState = function() {
const result = originalPushState.apply(this, arguments);
// React Native에 알림
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'urlChange',
url: window.location.href
}));
return result;
};
history.replaceState = function() {
const result = originalReplaceState.apply(this, arguments);
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'urlChange',
url: window.location.href
}));
return result;
};
// 뒤로가기 감지
window.addEventListener('popstate', function() {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'urlChange',
url: window.location.href
}));
});
})();
`;
[금액 파악]
캐시백은 구매 금액의 일정 퍼센트를 적립해야 하기 때문에 구매 금액을 파악해야 합니다.
네이버 스마트스토어의 경우 제휴사 API를 제공합니다. 이를 활용하면 주문번호만으로 주문 상세 정보, 주문 상태 추적을 할 수 있습니다.
스마트스토어로 입점했다면 해당 브랜드의 스마트스토어 API Key를 통해, 우리 앱에서 구매한 경우의 데이터만 주문 추적을 위해 활용했습니다.
문제는 카페24였습니다.
카페24의 경우 제휴사 API를 제공하지 않기에, 주문번호만 가지고 금액을 파악할 수 없었습니다.
가장 좋은 방법은 카페24 Webhook으로 주문 상세정보를 받는 것입니다.
하지만 이 방법은 카페24 앱스토어에 등록하는 앱(파트너 앱)을 추가로 개발해야 해서 개발 기간이 너무 길어집니다.
그래서 HTML 스크래핑을 사용했습니다.
HTML 스크래핑은 템플릿 변경 시 깨질 수 있으나, 그런 운영 리스크를 인지한 상태에서 선택했습니다.
카페24는 템플릿을 제공하기 때문에, 카페24 기반 쇼핑몰의 주요 요소들은 대부분 같은 클래스명을 가지고 있습니다.
그래서 구매 완료 페이지일 때 금액이 들어있는 특정 클래스명을 파악해 스크래핑하면 원하는 값을 얻을 수 있습니다.
const INJECTED_JAVASCRIPT = `
(function() {
var checkInterval = setInterval(function() {
var url = window.location.href;
// 구매 완료 페이지 여부
if (url.includes('order_result.html') || url.includes('pay-result')) {
// 금액 요소 찾기
var priceEl = document.querySelector('.totalPay .txtStrong')
|| document.querySelector('#total_order_price_view')
|| document.querySelector('.total_payment_price');
if (priceEl) {
var rawPrice = priceEl.innerText; // "30,000원"
var amount = parseInt(rawPrice.replace(/[^0-9]/g, ''), 10); // 30000
if (amount > 0) {
// React Native로 데이터 전송
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'purchaseComplete',
url: url,
amount: amount
}));
clearInterval(checkInterval);
}
}
}
}, 1000); // 1초마다 체크
})();
`;
[구매 확정 추적]
여기서 중요한 문제가 하나 더 있습니다.
'구매 완료'와 '구매 확정'은 다릅니다.
- 구매 완료: 결제가 끝난 상태
- 구매 확정: 배송 완료 후 사용자가 확정 버튼을 누름(또는 자동 확정)
만약 '구매 완료' 시점에 캐시백을 바로 지급한다면 사용자가 환불 및 구매 취소를 했을 때 캐시백은 이미 지급된 상태입니다.
그래서 '구매 확정' 시점에 캐시백을 지급해야 합니다.
이 문제를 해결하기 위해 사용자의 Wallet 테이블을 다음과 같이 세 부분으로 나누어 설계했습니다.
- balance_available: 출금 가능 금액
- balance_pending: 보류 금액(확정 대기)
- balance_cumulative:누적 적립 금액
구매가 완료되면 캐시백은 곧바로 지급되지 않고 balance_pending 상태로 보관됩니다.
이후 '구매 확정'이 되면 balance_available로 이동, '환불' 및 '취소'가 되면 balance_pending의 금액을 제거하는 식으로 캐시백 상태를 관리했습니다.
결국 이 구조가 제대로 동작하려면 '환불', '취소', '구매 확정'과 같은 주문 상태를 추적할 필요가 있습니다.
이건 브랜드몰의 주문 관리 시스템과 연동해서 주문 상태가 바뀔 때마다 우리 서버에 Webhook 방식으로 알려주는 것이 가장 이상적입니다.
하지만 저희 서비스에 입점할 대부분 브랜드에 개발자가 없는게 현실이었습니다.
네이버 스마트스토어의 경우 제공되는 API를 적극적으로 활용했습니다.
우리 앱에서 감지한 주문번호를 DB에 저장한 뒤 주기적으로 조회합니다.
주문 상태 변화에 따라 캐시백 지급 또는 취소 로직을 처리했습니다.
카페24의 경우 파트너 앱 기반 Webhook 방식은 개발 기간이 너무 길어지기 때문에 현실적인 타협을 했습니다.
일단 구매 완료 시점에 주문번호와 금액만 DB에 저장합니다.
브랜드사에는 해당하는 주문번호들에 대해 정기적으로 엑셀 파일을 보내달라 요청합니다.
이후 주문 상태가 변화했다면 수동으로 구매 확정/환불 처리(또는 엑셀 자동화)를 진행합니다.
완벽하진 않지만, 당장 서비스를 런칭하려면 이 방법이 최선이라 생각했습니다.
3. Deep Link 처리
구매 추적을 구현하면서 예상치 못한 문제가 또 있었습니다.
카드 결제 앱(KB Pay, 삼성 페이 등)으로 넘어가야 하는데, 인앱브라우저 안에서 전혀 실행되지 않는 문제였습니다.
Android는 결제 모듈 연동 시 'intent://' Scheme을 많이 사용하는데, 기본 WebView는 이걸 그대로 열어주지 않습니다.
그래서 WebView의 onShouldStartLoadWithRequest를 이용해 intent URL을 직접 파싱해서 처리하는 방식을 사용했습니다.
const onShouldStartLoadWithRequest = (request: any) => {
const { url } = request;
// 일반 웹페이지는 WebView에서 로드
if (url.startsWith('http://') || url.startsWith('https://')) {
return true;
}
// Android Intent 처리
if (Platform.OS === "android" && url.startsWith("intent:")) {
try {
// [시도 1] 원래 Intent URL 실행 시도
Linking.openURL(url).catch(async (err) => {
try {
// URL에서 scheme과 package 추출 (예: scheme=kb-acp, package=com.kbcard.cxh.appcard)
const schemeMatch = url.match(/scheme=([^;]+)/);
const packageMatch = url.match(/package=([^;]+)/);
const scheme = schemeMatch ? schemeMatch[1] : null;
const packageName = packageMatch ? packageMatch[1] : null;
// [시도 2] Intent 대신 App Scheme으로 직접 실행 시도 (예: kb-acp://...)
// intent://... 부분을 scheme://... 로 바꿔서 실행하면 잘 열리는 경우가 많음
if (scheme) {
const schemeUrl = url.replace("intent://", `${scheme}://`);
const cleanSchemeUrl = schemeUrl.split("#Intent;")[0];
try {
await Linking.openURL(cleanSchemeUrl);
return;
} catch (schemeErr) {
console.log("Scheme open failed, falling back to market");
}
}
// [시도 3] 앱 미설치로 간주하고 마켓으로 이동
if (packageName) {
Linking.openURL(`market://details?id=${packageName}`);
}
} catch (parseErr) {
console.log("Failed to parse intent");
}
});
} catch (e) {
console.error("Intent handling error:", e);
}
return false; // 웹뷰 로딩 차단
}
// iOS 및 기타 스킴: OS에 위임
if (Platform.OS === "ios") {
Linking.openURL(url).catch(() => {
console.log("App not installed or scheme not supported");
});
return false;
}
return false;
};
AndroidManifest.xml에도 쿼리 권한을 추가해줍니다.
android/app/src/main/AndroidManifest.xml
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https"/>
</intent>
<package android:name="com.kbcard.cxh.appcard" />
<package android:name="com.samsung.android.spay" />
</queries>
iOS는 intent 대신 커스텀 URL Scheme을 사용하는 방식이라, WebView에서 가로채지 않고 단순히 OS에 위임하는 방식으로 처리했습니다.
대신 Info.plist에 허용할 Scheme들을 미리 명시해두면 됩니다.
<key>LSApplicationQueriesSchemes</key>
<array>
<string>kb-acp</string>
<string>samsungpay</string>
</array>
4. 마무리하며
캐시백의 핵심 기능인 구매 추적 시스템을 만들면서 현실과 타협을 많이 했습니다.
저는 처음에 모든 브랜드가 Webhook을 제공하고, API로 실시간 주문 상태를 동기화하는 것을 생각했습니다.
하지만 현실은 개발자가 없는 브랜드가 대부분이었고, HTML 스크래핑이나 엑셀 수동 업로드 방식도 사용하게 됐습니다.
그렇지만 이렇게라도 작동하는 시스템을 만들었고 실제로 구매 추적이 됩니다.
카페24 서비스 앱처럼 완벽한 시스템은 나중에 개발 리소스가 충분할 때 전환하면 된다고 생각합니다.
현실적인 한계속에서 플랫폼마다 다른 전략을 찾아가는 과정에서 스스로 크게 성장할 수 있었습니다.
다음 글에서는 ReactNative의 Scrollview와 Flatlist에 대해 다루겠습니다.
'개발 > 주니어 개발자의 캐시백 앱 단독 개발기' 카테고리의 다른 글
| 8. 캐시백 적립 시 쿼리 무효화가 항상 정답은 아니었다 (0) | 2026.02.09 |
|---|---|
| 7. 편했던 ScrollView를 FlatList로 바꾸게 된 이유 (0) | 2026.02.09 |
| 5. 인프라 입문: Android HTTP 차단과 HTTPS 적용기 (1) | 2026.01.21 |
| 4. 갑자기 해외 서비스도 추가된다고? (0) | 2026.01.19 |
| 3. 앱 소셜 로그인의 첫 관문 - SHA-1과 키 해시 (0) | 2026.01.07 |