Next.js 광고 스크립트 hoist 버그 수정과 폴링 안정화
목차
Next.js 환경에서 서드파티 광고 스크립트가 조용히 씹히는 버그를 잡았다.
배경 — Script hoist 가 광고를 죽이고 있었다
Next.js의 <Script> 컴포넌트는 편하다. strategy="afterInteractive" 하나면 로딩 순서 걱정 없이 스크립트를 붙일 수 있고, 개발자 입장에서는 그냥 믿고 쓰게 된다. 그런데 이 편함이 함정이기도 하다.
MobonInner 같은 광고 솔루션은 스크립트가 특정 DOM 위치에 인라인으로 존재해야 제대로 동작하는 경우가 있다. Next.js <Script>는 내부적으로 해당 스크립트를 <head> 또는 문서 하단으로 hoist(끌어올리거나 내려버리기)해서 삽입한다. 컴포넌트 트리 안에 <Script>를 넣어도 실제 DOM에서는 전혀 다른 위치에 박힌다는 뜻이다. 이 동작 자체는 성능 최적화 의도로 만들어진 것이지만, 광고 스크립트처럼 "실행 위치"와 "실행 타이밍"이 동시에 중요한 케이스에서는 치명적이다.
증상은 단순했다. 슬롯은 렌더링되는데 광고가 안 붙는다. 콘솔 에러도 없다. 그냥 빈 칸.
작업 내용 — div 안 inline script + 폴링으로 우회
AdSlot.tsx 하나만 건드렸지만 변경 내용은 두 갈래로 나뉜다.
첫 번째, <Script> 컴포넌트를 버리고 <div dangerouslySetInnerHTML> 방식으로 인라인 스크립트를 직접 주입했다.
// Before — Next.js Script (hoist 발생)
<Script strategy="afterInteractive" src="..." />
// After — div 안 inline script, hoist 없음
<div
dangerouslySetInnerHTML={{
__html: `<script type="text/javascript">/* MobonInner 초기화 코드 */</script>`,
}}
/>
dangerouslySetInnerHTML은 이름부터 경고를 주는 API다. XSS 위험 때문에 React가 이렇게 네이밍한 거고, 실제로 외부 사용자 입력이 닿는 곳에 쓰면 안 된다. 그런데 광고사에서 제공하는 고정된 스니펫을 그대로 넣는 케이스는 다르다. 내용이 변하지 않고, 외부 입력이 개입하지 않으며, hoist 없이 정확한 위치에 삽입해야 한다는 요구사항을 만족하는 방법이 이것 말고는 마땅치 않았다. 팀 리뷰 때도 "왜 dangerouslySetInnerHTML을 쓰는가"는 반드시 짚고 가야 할 포인트였고, 위 맥락을 주석으로 남겨뒀다.
두 번째, HawkEyes 로드 여부를 폴링으로 확인하는 로직을 추가했다.
useEffect(() => {
const MAX_RETRY = 20;
let count = 0;
const timer = setInterval(() => {
count += 1;
if (typeof window.HawkEyes !== "undefined") {
clearInterval(timer);
// HawkEyes 초기화 / 슬롯 등록
}
if (count >= MAX_RETRY) {
clearInterval(timer);
// 로드 실패 처리
}
}, 200);
return () => clearInterval(timer);
}, []);
HawkEyes는 MobonInner가 내부적으로 의존하는 전역 객체다. 인라인 스크립트를 DOM에 박아도 외부 JS 리소스가 비동기로 로드되는 타이밍이 있어서, window.HawkEyes가 즉시 존재한다는 보장이 없다. 이벤트 훅이 없는 서드파티 스크립트에서 흔히 쓰는 패턴이 이 폴링이다. 인터벌 200ms, 최대 20회(4초) 시도 후 포기하는 구조로, 클린업도 return () => clearInterval(timer)로 확실히 잡았다.
회고
| 항목 | 기존 방식 | 변경 후 |
|---|---|---|
| 스크립트 삽입 | <Script> (hoist O) |
dangerouslySetInnerHTML (hoist X) |
| HawkEyes 의존 처리 | 없음 (타이밍 불안정) | 폴링 + 타임아웃 |
| 실패 시 동작 | 조용히 빈 슬롯 | 최대 재시도 후 명시적 포기 |
이런 버그가 까다로운 이유는 "동작하는 것처럼 보이는" 구간이 없다는 것이다. 광고 슬롯이 렌더는 되는데 광고가 없으면, 수익 손실은 발생하지만 에러 로그로는 아무것도 잡히지 않는다. 모니터링 지표에서 노출률이 떨어지거나 광고사 대시보드에서 이상을 확인하기 전까지는 인지 자체가 늦다.
서드파티 광고 스크립트를 Next.js App에 붙일 때 <Script> 컴포넌트가 만능이 아니라는 걸 이번에 다시 확인했다. hoist 동작을 충분히 이해하고 붙여야 하고, 외부 전역 객체 의존이 있는 스크립트라면 단순 삽입만으로는 부족하다. 폴링이 우아한 해법은 아니지만, 이벤트 콜백을 제공하지 않는 서드파티 환경에서는 현실적인 선택지다.
AdSlot.tsx 한 파일 수정이었지만, 광고 노출 안정성 전반에 영향을 주는 작업이었다.
끝.
🛒 이 글과 어울리는 추천 상품
*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.
댓글 0
첫 댓글 달아줘.