본문으로 건너뛰기

SPA 광고 적용 (React)

SDK 로드​

애플리케이션의 인덱스 페이지에서 NAM SDK를 로드합니다.

<!DOCTYPE html>
<html lang="en">
<head>
<title>React App</title>
<script async src="https://sdk.naverads.net/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를 사용해 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',
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} />
);
}