SPA 광고 적용 (React)
SDK 로드
애플리케이션의 인덱스 페이지에서 NAM SDK를 로드합니다.
<!DOCTYPE html>
<html lang="en">
<head>
<title>React App</title>
<script async src="https://ssl.pstatic.net/tveta/libs/glad/prod/gfp-core.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
NAM SDK는 반드시 한번만 로드되어야 합니다.
이를 보장하기 위해 초기 index.html
템플릿의 <head>
태그 내에 스크립트를 추가해 SDK를 로드하는 것을 권장하지만, React 컴포넌트 내부에서 동적으로 SDK를 로드하려는 경우, SDK가 중복으로 로드되지 않도록 주의하세요.
SDK 호출
NAM SDK의 메소드 호출 시, SDK가 초기화가 완료된 것을 보장하기 위해, 명령어 대기열을 사용해야합니다.
React에서는 Cutsom Hook을 정의하면, 매번 명령어 대기열을 사용하지 않고도 더 쉽게 SDK 메소드를 호출할 수 있습니다.
- Suspense
- State
Suspense를 사용해 SDK가 초기화 될 때까지 렌더링을 지연시키는 방법입니다. SDK의 초기화 상태를 보장하며 Type-Safe하게 SDK 메소드에 접근할 수 있습니다.
let sdk;
window.gladsdk = (window.gladsdk || { cmd: [] });
const suspender = new Promise((resolve) => {
window.gladsdk.cmd.push(resolve);
}).then(() => {
sdk = window.gladsdk;
});
export function useGladSdk() {
if (!sdk) {
throw suspender;
}
return sdk;
}
import { Suspense } from 'react';
import { useGladSdk } from './use-glad-sdk';
const MyAd = () => {
const gladSdk = useGladSdk();
useEffect(() => {
const adSlot = gladSdk.defineAdSlot({
adUnitId: 'ad_unit_id',
adSlotElementId: 'slot',
});
gladSdk.displayAd(adSlot)
}, [gladSdk]);
return <div id="slot" />
}
const MyComponent = () => {
return (
// SDK가 초기화되지 않은 경우 fallback 렌더링 됨
<Suspense fallback={null}>
<MyAd />
</Suspense>
)
}
Suspense의 사용을 원하지 않는 경우, State를 통해 SDK 초기화 여부를 확인 후 SDK 메소드를 호출할 수도 있습니다.
import { useState } from 'react';
let sdk;
const initListeners: (() => void)[] = [];
window.gladsdk = (window.gladsdk || { cmd: [] });
window.gladsdk.cmd.push(() => {
sdk = window.gladsdk;
initListeners.forEach(listener => listener());
});
export function useGladSdk() {
const [gladSdk, setGladSdk] = useState(sdk);
useEffect(() => {
if (gladSdk) return;
initListeners.push(() => {
setGladSdk(sdk);
});
}, [gladSdk])
return gladsdk;
}
import { useGladSdk } from './use-glad-sdk';
const MyComponent = () => {
const gladSdk = useGladSdk();
useEffect(() => {
if (!gladSdk) return; // SDK가 초기화되지 않은 경우 undefined
const adSlot = gladSdk.defineAdSlot({
adUnitId: 'ad_unit_id',
adSlotElementId: 'slot',
});
gladSdk.displayAd(adSlot)
}, [gladSdk]);
return <div id="slot" />
}
아래 예제들은 모두 Suspense를 기반으로 한 useGladSdk
를 기준으로 제작되었습니다.
State 기반의 useGladSdk
를 사용하는 경우 gladSdk
에 접근할 때 항상 위의 예제처럼 gladSdk
의 존재 유무를 확인하세요. 실수를 방지하기 위해 Typescript의 strict 옵션을 true
로 설정하는 것을 권장합니다.
광고 슬롯 정의
여러 광고 단위를 게재하려는 경우 광고 슬롯을 관리하는 Hook과 컴포넌트를 정의해 코드의 재사용성을 높일 수 있습니다.
import { useEffect, useRef } from 'react';
import { useGladSdk } from './use-glad-sdk';
type AdSlotInfo = Record<string, any>;
type AdEventListener = (ad: any, error?: unknown) => void;
interface AdEventListeners {
onAdLoaded?: AdEventListener;
onAdClicked?: AdEventListener;
onAdImpressed?: AdEventListener;
onAdMuteCompleted?: AdEventListener;
onError?: AdEventListener;
}
const defaultListeners = {};
/**
* 주어진 AdSlotInfo에 대해 AdSlot을 생성하고,
* 해당 AdSlot에 대해 이벤트 리스너를 추가하는 Hook
*/
export function useAdSlot(adSlotInfo: AdSlotInfo, eventListeners: AdEventListeners = defaultListeners) {
const gladSdk = useGladSdk();
const [adSlot, setAdSlot] = useState(() => gladSdk.defineAdSlot(adSlotInfo));
const adSlotInfoRef = useRef<AdSlotInfo>();
const eventListenersRef = useRef(eventListeners);
if (!deepEqual(adSlotInfoRef.current, adSlotInfo)) {
adSlotInfoRef.current = adSlotInfo;
}
const optimalAdSlotInfo = adSlotInfoRef.current;
useEffect(() => {
const _adSlot = gladSdk.defineAdSlot(optimalAdSlotInfo);
setAdSlot(_adSlot);
return () => {
gladSdk.destroyAdSlots([_adSlot]);
};
}, [gladSdk, optimalAdSlotInfo]);
useEffect(() => {
eventListenersRef.current = eventListeners;
}, [eventListeners]);
useEffect(() => {
const eventMap: Record<string, string> = {
[gladSdk.event.AD_LOADED]: 'onAdLoaded',
[gladSdk.event.AD_CLICKED]: 'onAdClicked',
[gladSdk.event.AD_IMPRESSED]: 'onAdImpressed',
[gladSdk.event.AD_MUTE_COMPLETED]: 'onAdMuteCompleted',
[gladSdk.event.ERROR]: 'onError',
};
const disposers = Object.entries(eventMap).map(([event, listenerName]) => {
const listener: AdEventListener = (ad, error) => {
if (ad.slot !== adSlot) return;
eventListenersRef.current[listenerName]?.(ad, error);
}
gladSdk.addEventListener(event, listener);
return () => gladSdk.removeEventListener(event, listener);
});
return () => {
disposers.forEach((dispose) => dispose());
};
}, [gladSdk, adSlot]);
return adSlot;
};
function deepEqual(a: unknown, b: unknown): a is typeof b {
// implement your deep compare method for better stability.
return JSON.stringify(a) === JSON.stringify(b)
}
import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from 'react';
type NaverAdRef = ComponentRef<'div'>;
type NaverAdProps = ComponentPropsWithoutRef<'div'> & {
adSlot: any;
};
/**
* AdSlot을 props로 받아 광고를 노출할 Element를 생성하는 컴포넌트
*/
export const NaverAd = forwardRef<NaverAdRef, NaverAdProps>((props, ref) => {
const { adSlot, ...divProps } = props;
return <div {...divProps} ref={ref} id={adSlot.getAdSlotElementId()} />;
});
NaverAd.displayName = 'NaverAd';
광고 노출
정의한 Hook들과 컴포넌트를 이용해 광고를 노출합니다.
import { useEffect } from 'react';
import { NaverAd } from './NaverAd';
import { useAdSlot } from './use-ad-slot';
import { useGladSdk } from './use-glad-sdk';
const adSlotInfo = {
adUnitId: 'ad_unit_id',
adSlotElementId: 'slot',
uct: 'KR',
customParam: {
category: 'entertainment',
hobby: ['music', 'sports'],
},
};
const MyComponent = () => {
const gladSdk = useGladSdk();
const adSlot = useAdSlot(adSlotInfo, {
onAdLoaded(ad: any) {
// @TODO implements code
},
onAdClicked(ad: any) {
// @TODO implements code
},
});
useEffect(() => {
gladSdk.displayAd(adSlot);
}, [gladSdk, adSlot]);
return (
<NaverAd adSlot={adSlot} />
);
}