devlog

애드센스가 못 찾은 스크립트 태그, 그리고 React가 옮겨버린 순서

  • next.js
  • react
  • adsense
  • gdpr

계산기 모음 사이트를 배포하고 애드센스 승인을 받으려는데 이 화면에서 막혔다.

사이트를 확인할 수 없습니다. 사이트의 변경사항이 게시되었고 Google 애드센스 크롤러가 이를 액세스할 수 있는지 확인하세요.

스니펫은 분명히 넣었고 사이트는 잘 떠 있었다.

첫 번째 가설은 틀렸다

문구가 "크롤러가 액세스할 수 있는지 확인하세요"라고 하니 자연스럽게 차단을 의심했다. Cloudflare Workers에 올렸으니 봇 보호가 걸렸을 수 있다. 확인은 간단하다.

for ua in "Mediapartners-Google" "AdsBot-Google" "Googlebot/2.1"; do
  curl -s -o /dev/null -w '%{http_code}\n' -A "$ua" https://example.com/
done

셋 다 200이었다. 가설을 버렸다.

여기서 시간을 아꼈다고 생각한다. 오류 문구가 지목하는 방향이 늘 원인은 아니고, 확인 비용이 3분이면 추측보다 재는 게 빠르다.

진짜 원인: 태그가 HTML에 없었다

브라우저 개발자 도구에서는 스크립트가 멀쩡히 보였다. 그런데 서버가 내려주는 원본 HTML을 직접 받아보니 달랐다.

curl -s https://example.com/ | grep -o '<script[^>]*adsbygoogle[^>]*>'
# (아무것도 안 나옴)

curl -s https://example.com/ | grep -o 'rel="preload"[^>]*adsbygoogle'
# rel="preload" ... adsbygoogle

<script>는 없고 <link rel="preload">만 있었다.

next/script의 기본 전략인 afterInteractive는 스크립트를 브라우저에서 JS로 주입한다. 서버 HTML에는 미리 받아두라는 힌트만 남는다. 개발자 도구는 주입이 끝난 뒤의 DOM을 보여주니 멀쩡해 보였던 것이다.

애드센스 확인기는 JS를 실행하지 않고 HTML 원문을 훑는다. 없는 걸 찾고 있었다.

그래서 일반 태그로 바꿨다.

export function AdSenseScript() {
  return (
    <script
      async
      crossOrigin="anonymous"
      src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${CLIENT}`}
    />
  );
}

고치자마자 다른 게 깨졌다

EEA·영국 트래픽에는 동의 없이 광고 쿠키를 쓸 수 없다. 그래서 Consent Mode 기본값을 <body> 맨 위 인라인 스크립트로 두고 있었다. 인라인 스크립트는 파서가 닿는 즉시 실행되니, 나중에 로드되는 광고 스크립트보다 앞선다는 게 보장된다 — 문서 순서만 지키면.

빌드 결과에서 두 스크립트의 위치를 재봤다.

| | 위치 | |---|---| | 애드센스 로더 | 1251 | | 동의 기본값 | 3277 | | </head> | 3073 |

순서가 뒤집혀 있었다. React 19는 <script async src><head>로 끌어올린다. <body> 어디에 두든 소용이 없다. 동의 기본값이 광고 태그 뒤로 밀렸다.

<head>에 직접 넣어도 안 됐다

그러면 동의 스크립트도 <head>에 넣으면 되지 않나. 넣어봤다.

| | 위치 | |---|---| | 애드센스 로더 | 1251 | | 동의 기본값 | 3090 |

여전히 뒤였다. 호이스팅된 스크립트는 명시적으로 렌더링한 <head> 자식보다도 앞에 꽂힌다. 배치로는 이길 수 없는 싸움이었다.

async를 버렸다

React가 끌어올리는 대상은 async 스크립트다. defer는 호이스팅 대상이 아니다.

<script
  defer                    // async 였다
  crossOrigin="anonymous"
  src={SRC}
/>

| | 위치 | |---|---| | 동의 기본값 | 2940 | | 애드센스 로더 | 3487 | | </head> | 3616 |

순서가 잡혔다. 둘 다 <head> 안에 있고, defer는 적힌 자리에 남아 문서 순서대로 실행된다.

기능상 손해도 거의 없다. defer 스크립트는 파싱이 끝난 뒤 실행되는데, 광고 슬롯은 어차피 window.adsbygoogle 큐에 밀어 넣는 방식이라 로더가 조금 늦게 와도 정상적으로 채워진다. 애드센스는 이 상태로 사이트 확인을 통과했다.

남은 것

세 가지를 배웠다.

개발자 도구가 보여주는 DOM은 서버가 보낸 HTML이 아니다. 크롤러를 상대할 때는 curl로 원문을 봐야 한다. 이걸 처음부터 했으면 30분을 아꼈다.

프레임워크의 최적화는 내가 정한 순서를 다시 정할 수 있다. async를 붙인 건 "빨리 받아라"는 뜻이었는데, React는 "그럼 맨 앞으로 올려주지"로 읽었다. 순서가 계약의 일부라면 그 계약을 코드에 적어둬야 한다.

호이스팅을 피하는 방법이 defer라는 건 코드만 봐서는 절대 안 보인다. 그래서 컴포넌트 주석과 프로젝트 문서 양쪽에 왜 async가 아닌지를 적었다. 안 적어두면 누군가 "왜 next/script를 안 쓰지?" 하고 되돌릴 코드다. 나 자신을 포함해서.