주문 추적을 구현한 후, 실제로 구매를 테스트할 수 있었습니다.
앱에서 브랜드몰로 들어가 상품을 구매하고, 구매 완료 페이지까지 도달했습니다.
백엔드 로그를 확인하면 주문번호가 제대로 매칭되고 DB에서 캐시백도 적립되었는데, 캐시백 카드의 잔액은 계속 0원이었습니다.
앱을 종료하고 다시 열었을 때만 갱신되었던 것입니다.
1. 쿼리 갱신하기
적립되는 순간에 화면을 갱신해주면 되겠다는 생각이 가장 먼저 떠올랐습니다.
프론트엔드에서는 React Query를 사용하고 있었고, 사용자 정보와 지갑 잔액은 ['me'] 쿼리로 관리되었습니다.
주문 추적 API가 성공하는 순간, 이 쿼리의 캐시를 무효화하면 자동으로 새로운 잔액을 가져올 것 같았습니다.
// 인앱 브라우저 내 구매 추적 -> 캐시백 잔액 자동 갱신
const trackConversion = useMutation({
mutationFn: (trackingData) => api.post('/orders/track-purchase', trackingData),
onSuccess: () => {
// 사용자 피드백
Toast.show({
type: "success",
text1: "구매 확인 완료",
text2: "캐시백 적립까지 1~2분 소요됩니다."
});
// 잔액 갱신을 위해 'me' 쿼리 무효화
queryClient.invalidateQueries({ queryKey: ['me'] });
},
});
하지만 테스트 결과, 캐시백이 적립되더라도 여전히 잔액카드는 0원이었습니다.
2. 문제의 진짜 원인: 무효화 시점
왜 잔액이 갱신되지 않았는지 파악하기 위해 백엔드 흐름을 다시 살펴보았습니다.
캐시백 적립은 trackConversion API 자체가 직접 처리하는 게 아니었습니다.
trackConversion은 주문번호를 백엔드에 등록하는 역할에 불과했고, 실제 캐시백 적립은 백엔드의 TasksService가 1분 주기로 네이버 스마트스토어 API를 호출하여 주문 상태를 확인한 후에 처리되었습니다.
invalidateQueries가 발동되는 시점은 trackConversion이 성공하는 즉시였습니다.
하지만 그 시점에는 아직 TaskService가 실행되지 않았고, Wallet도 갱신되지 않은 상태입니다.
결과적으로 서버에서 가져온 잔액은 갱신 전의 잔액인 0원이 됩니다.
"쿼리 무효화를 했는데 왜 안 바뀌지?"라고 생각했던 건, 문제가 쿼리 무효화 자체가 아니라 무효화 시점이었기 때문입니다.
[프론트엔드] [백엔드]
구매 완료 감지
↓
trackConversion 호출 ───────► 주문번호 등록 (DB 저장)
↓ ↓
onSuccess 발동 ↓ (1분 후)
invalidateQueries(['me']) ◄─── TasksService 실행
↓ 주문 상태 확인 및 Wallet 갱신
'me' 쿼리 갱신
↓
잔액카드 업데이트(TasksService 이전 시점)
3. 해결 방법: 무음 푸시를 활용한 갱신
원인을 파악한 후, 두 가지 방법을 생각했습니다.
방법 1) setTimeout으로 지연 후 무효화
가장 간단한 방법이었습니다. trackConversion 성공 시점에서 1분, 2분 후에 각각 무효화하는 것이죠.
onSuccess: () => {
// 1분 후 무효화
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: ['me'] });
}, 60000);
// 2분 후 무효화
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: ['me'] });
}, 120000);
},
이 방식은 구현은 간단하지만 사용자가 앱을 백그라운드로 보내거나 종료하면 setTimeout 타이머가 중지되거나 사라지는 문제가 있습니다. 정확한 타이밍을 보장할 수 없는 방식이기 때문에 사용하지 않았습니다.
방법 2) 무음 푸시 알림을 활용한 갱신
다음으로 떠올린 건 백엔드가 Wallet을 갱신한 후에 프론트엔드에 알려주는 방향이었습니다.
백엔드에서 프론트엔드로 이벤트를 먼저 보내야 하기 때문에 보통 웹소켓이나 무음 푸시 알림을 사용합니다.
어차피 이 앱에는 알림 기능이 필요한 상황이었습니다.
캐시백 적립, 주문 상태 변화 알림 등 백엔드에서 특정 유저에게 실시간 이벤트를 보내야 하는 기능이 앞으로 개발될 예정이었죠.
백엔드에서는 Wallet이 갱신되는 직후에 NotificationService를 호출하여 해당 유저에게 WALLET_UPDATED 타입의 무음 푸시를 보냅니다.
프론트엔드는 이 이벤트를 수신하는 순간에 invalidateQueries를 호출합니다.
그렇게 하면 정확한 타이밍에 갱신이 이루어집니다.
[백엔드] [프론트엔드]
TasksService 실행 (1분 주기)
↓
주문 상태 확인
↓
Wallet 갱신
↓
NotificationService 호출 ───────► 무음 푸시 수신
(type: WALLET_UPDATED) ↓
(user_id: 해당 유저) invalidateQueries(['me'])
↓
잔액카드 자동 갱신
4. 배운 점
이 문제를 겪기 전까지는 invalidateQueries를 API 성공 시점에 붙이면 자동으로 갱신되는 것이라고 생각했습니다.
하지만 실제로는 쿼리 무효화 시점과 실제 데이터 갱신 시점이 다를 수 있다는 것을 깨달았습니다.
특히 백엔드에 외부 API 호출, 비동기 처리 로직, 주기적인 배치 작업과 같은 요소들이 섞이기 시작하면, 프론트엔드는 더 이상 갱신 시점을 정확히 알기 어렵습니다.
이 문제는 단순히 쿼리 무효화를 넘어서서 백엔드와 프론트엔드 사이의 타이밍을 다루는 질문이었습니다.
그 답은 '프론트엔드가 백엔드의 타이밍을 추측하는 것'이 아닌 '백엔드가 갱신되었다는 정보를 프론트엔드에 직접 알려주는 것'입니다.
특히 이번 경험을 통해 배운 점은
트랜잭션이 점점 복잡해질수록 무엇이 먼저 일어나야 하는지, 순서를 명확히 정리하지 않으면 안 된다는 것이었습니다.
- 주문이 등록되는 시점
- 외부 API를 통해 적립이 확인되는 시점
- Wallet이 실제로 변경되는 시점
- 프론트엔드가 그 변경을 반영해도 되는 시점
이 네 단계가 모두 다른 만큼 순서를 명확히 파악해야 합니다.
그렇지 않으면 쿼리 무효화는 아직 바뀌지 않은 데이터를 다시 가져올 뿐이니까요.
다음 글에서는 실제로 푸시 알림 시스템을 구축하는 과정에서 겪은 트러블슈팅을 다루겠습니다.
'개발 > 주니어 개발자의 캐시백 앱 단독 개발기' 카테고리의 다른 글
| 7. 편했던 ScrollView를 FlatList로 바꾸게 된 이유 (0) | 2026.02.09 |
|---|---|
| 6. 인앱브라우저에서 주문을 추적하는 법 (0) | 2026.01.29 |
| 5. 인프라 입문: Android HTTP 차단과 HTTPS 적용기 (1) | 2026.01.21 |
| 4. 갑자기 해외 서비스도 추가된다고? (0) | 2026.01.19 |
| 3. 앱 소셜 로그인의 첫 관문 - SHA-1과 키 해시 (0) | 2026.01.07 |