안녕하세요. 도움 주셔서 정말 감사드립니다.
cleanup 호출 시점과 loadFullScreenAd 위치에 대한 간략한 설명 먼저 공유드립니다.
cleanup 호출 시점
loadFullScreenAd의 cleanup(unregister)은 아래 두 경우에만 호출됩니다:
load() 재호출 직전 — 중복 구독 방지를 위해 기존 구독을 먼저 해제하고 새로 등록
- 컴포넌트 언마운트 시 —
useEffect cleanup과 reset() 에서 호출
show() 호출 시에는 loadFullScreenAd cleanup을 호출하지 않습니다. load 구독은 show가 끝날 때까지 살아있습니다.
loadFullScreenAd 호출 위치
커스텀 훅 useRewardedAd의 load() 함수 내부에 있고, 이 load()는 채팅 페이지 컴포넌트 마운트 시 useEffect(() => { rewardedAd.load(); }, []) 로 한 번 호출됩니다.
참고 사항
@apps-in-toss/framework 2.4.7 (RN) 사용 중입니다. 답변 주신 예시는 @apps-in-toss/web-framework 기준인 것 같은데, RN과 web 간에 showFullScreenAd 동작에 차이가 있을 수 있을까요?
loaded 이벤트 수신(isLoaded = true) 후 즉시 show()를 호출해도 동일하게 failedToShow가 발생합니다.
혹시 위 패턴에서 잘못된 부분이 있으면 알려주시면 감사하겠습니다.
아래는 전체 코드(src/components/ad-gate/useRewardedAd.ts) 공유드립니다.
import { useCallback, useEffect, useRef, useState } from 'react';
import { loadFullScreenAd, showFullScreenAd } from '@apps-in-toss/framework';
export interface RewardData {
unitType: string;
unitAmount: number;
}
interface UseRewardedAdOptions {
adGroupId: string;
onReward: (data: RewardData) => void;
onDismissed?: () => void;
onError?: (err: unknown) => void;
}
interface UseRewardedAdResult {
load: () => void;
show: () => void;
reset: () => void;
isLoaded: boolean;
isLoading: boolean;
isShowing: boolean;
error: unknown | null;
}
/**
* 앱인토스 통합 광고 SDK(`loadFullScreenAd` / `showFullScreenAd`)를 보상형(rewarded) 용도로 감싼 훅.
*
* 사용 흐름:
* 1) `load()` 호출 → `isLoaded`가 true가 되면 광고 재생 준비 완료
* 2) `show()` 호출 → 광고 재생
* - 시청 완료 시 `onReward` (=> sendAdvertiseFinish 트리거)
* - 보상 없이 닫음/실패 시 `onDismissed` / `onError`
* 3) `reset()`으로 재사용 전에 초기화
*
* 주의:
* - adGroupId는 앱인토스 콘솔에서 생성한 광고 그룹 ID. 개발/QA는 공식 테스트 ID 사용.
* - SDK 미지원 환경(웹 미리보기 등)에서는 `onError` 콜백으로 실패 통지 후 no-op.
* - 언마운트 시 load/show 구독이 cleanup 된다.
*/
export function useRewardedAd({
adGroupId,
onReward,
onDismissed,
onError,
}: UseRewardedAdOptions): UseRewardedAdResult {
const [isLoaded, setIsLoaded] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isShowing, setIsShowing] = useState(false);
const [error, setError] = useState<unknown | null>(null);
const loadCleanupRef = useRef<(() => void) | null>(null);
const showCleanupRef = useRef<(() => void) | null>(null);
const rewardReceivedRef = useRef(false);
const _cleanupLoad = useCallback(() => {
loadCleanupRef.current?.();
loadCleanupRef.current = null;
}, []);
const _cleanupShow = useCallback(() => {
showCleanupRef.current?.();
showCleanupRef.current = null;
}, []);
const reset = useCallback(() => {
_cleanupLoad();
_cleanupShow();
setIsLoaded(false);
setIsLoading(false);
setIsShowing(false);
setError(null);
rewardReceivedRef.current = false;
}, [_cleanupLoad, _cleanupShow]);
const load = useCallback(() => {
if (!loadFullScreenAd.isSupported()) {
const err = new Error('Rewarded ad is not supported in this environment');
setError(err);
onError?.(err);
return;
}
if (isLoading || isLoaded) return;
setIsLoading(true);
setError(null);
_cleanupLoad();
loadCleanupRef.current = loadFullScreenAd({
options: { adGroupId },
onEvent: (event) => {
if (event.type === 'loaded') {
setIsLoaded(true);
setIsLoading(false);
}
},
onError: (err) => {
setIsLoading(false);
setIsLoaded(false);
setError(err);
onError?.(err);
},
});
}, [adGroupId, isLoaded, isLoading, _cleanupLoad, onError]);
const show = useCallback(() => {
if (!showFullScreenAd.isSupported()) {
const err = new Error('Rewarded ad is not supported in this environment');
setError(err);
onError?.(err);
return;
}
if (!isLoaded || isShowing) return;
setIsShowing(true);
rewardReceivedRef.current = false;
_cleanupShow();
showCleanupRef.current = showFullScreenAd({
options: { adGroupId },
onEvent: (event) => {
if (event.type === 'userEarnedReward') {
rewardReceivedRef.current = true;
onReward(event.data);
} else if (event.type === 'dismissed') {
setIsShowing(false);
setIsLoaded(false);
if (!rewardReceivedRef.current) {
onDismissed?.();
}
} else if (event.type === 'failedToShow') {
setIsShowing(false);
setIsLoaded(false);
const err = new Error('Ad failed to show');
setError(err);
onError?.(err);
}
},
onError: (err) => {
setIsShowing(false);
setIsLoaded(false);
setError(err);
onError?.(err);
},
});
}, [adGroupId, isLoaded, isShowing, _cleanupShow, onReward, onDismissed, onError]);
useEffect(() => {
return () => {
_cleanupLoad();
_cleanupShow();
};
}, [_cleanupLoad, _cleanupShow]);
return { load, show, reset, isLoaded, isLoading, isShowing, error };
}