본문으로 건너뛰기

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>
warning

NAM SDK는 반드시 한번만 로드되어야 합니다.

이를 보장하기 위해 초기 index.html 템플릿의 <head> 태그 내에 스크립트를 추가해 SDK를 로드하는 것을 권장하지만, React 컴포넌트 내부에서 동적으로 SDK를 로드하려는 경우, SDK가 중복으로 로드되지 않도록 주의하세요.

SDK 호출

NAM SDK의 메소드 호출 시, SDK가 초기화가 완료된 것을 보장하기 위해, 명령어 대기열을 사용해야합니다.

React에서는 Cutsom Hook을 정의하면, 매번 명령어 대기열을 사용하지 않고도 더 쉽게 SDK 메소드를 호출할 수 있습니다.

Suspense를 사용해 SDK가 초기화 될 때까지 렌더링을 지연시키는 방법입니다. SDK의 초기화 상태를 보장하며 Type-Safe하게 SDK 메소드에 접근할 수 있습니다.

use-glad-sdk.ts
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;
}
MyComponent.tsx
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>
)
}

광고 슬롯 정의

여러 광고 단위를 게재하려는 경우 광고 슬롯을 관리하는 Hook과 컴포넌트를 정의해 코드의 재사용성을 높일 수 있습니다.

use-ad-slot.ts
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)
}
NaverAd.tsx
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들과 컴포넌트를 이용해 광고를 노출합니다.

MyComponent.tsx
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} />
);
}