Gatsby SSR에서 Script API 적용 이슈와 해결

2024-09-22 · 2 min read

개요

Gatsby에서 Google AdSense 코드를 삽입하려 했으나 SSR 과정에서 Gatsby Script API가 예상대로 작동하지 않는 문제가 발생했습니다. 이 글에서는 문제의 원인과 해결 과정, 그리고 Gatsby Script의 내부 구현에 대해 간단히 살펴보고자 합니다.

문제 상황

구글 애드센스를 적용하기 위해 사이트 소유권을 확인해야 했고, 다음 방법 중 하나를 선택해야 했습니다.

  • 애드센스 코드 스니펫 삽입
  • Ads.txt 스니펫 삽입
  • 메타태그 삽입

Ads.txt나 메타태그로 소유권 인증을 하더라도, 이후 광고를 정상적으로 게재하려면 결국 애드센스 코드를 추가해야 했습니다. 그래서 저는 처음부터 애드센스 코드 스니펫을 삽입하는 방법을 선택했습니다.

이를 위해 Gatsby Script API를 사용했는데, 공식 문서에 Gatsby의 SSR에서 사용하는 예시가 있어 SSR에서도 사용 가능하다고 생각했습니다.

export const onRenderBody = ({
  setHeadComponents,
  setHtmlAttributes,
}: RenderBodyArgs) => {
  // ...

  setHeadComponents([
    <Script
      async
      src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=..."
      crossOrigin="anonymous"
    />,
  ]);
};

그러나 Google AdSense의 사이트 검증 과정에서 사이트를 확인할 수 없습니다라는 오류가 발생했습니다. SSR된 HTML을 확인해보니 Gatsby Script API로 삽입한 스크립트가 존재하지 않았습니다.

cannot-verify.png

문제 해결

이 문제는 Gatsby Script 컴포넌트를 React의 기본 <script>로 변경하여 해결할 수 있었습니다. 그러나 왜 Gatsby Script가 SSR에서 작동하지 않았는지 궁금했습니다.

Gatsby Script의 로드 전략

Gatsby Script는 strategy prop을 통해 스크립트 로드 전략을 지정할 수 있었습니다. 이 전략은 다음과 같은 세 가지가 있었습니다.

  • post-hydrate: React의 hydration 이후 스크립트를 로드합니다. (기본값)
  • idle: 메인 스레드가 유휴 상태일 때 스크립트를 로드합니다.
  • off-main-thread: Partytown 라이브러리를 통해 백그라운드 스레드에서 스크립트를 로드합니다.

Gatsby Script 내부 구현 분석

자세히 알아보기 위해 Gatsby Script의 내부 구현을 살펴봤습니다. GatsbyScript는 우리가 잘 아는 React Functional Component 형태였고, 각 전략별로 다음과 같은 특징이 있었습니다.

function GatsbyScript(props: ScriptProps): ReactElement | null {
  const { src, strategy = ScriptStrategy.postHydrate } = props || {}

  const { pathname } = useLocation()

  useEffect(() => {
    let details: IInjectedScriptDetails | null

    switch (strategy) {
      case ScriptStrategy.postHydrate:
        details = injectScript(props)
        break
      case ScriptStrategy.idle:
        requestIdleCallback(() => {
          details = injectScript(props)
        })
        break
      case ScriptStrategy.offMainThread:
        {
          const attributes = resolveAttributes(props)
          collectedScriptsByPage.set(pathname, attributes)
        }
        break
    }

    return (): void => {
      // inject한 스크립트를 clean up
    }
  }, [])

  if (strategy === ScriptStrategy.offMainThread) {
    const inlineScript = resolveInlineScript(props)
    const attributes = resolveAttributes(props)

    if (typeof window === `undefined`) {
      collectedScriptsByPage.set(pathname, attributes)
    }

    return (
      <script type="text/partytown" ... />
    )
  }

  return null
}

출처: gatsby-script.tsx

1. post-hydrate

컴포넌트의 리턴값은 null이었습니다. 대신 useEffect 내에서 DOM API를 사용해 스크립트를 동적으로 삽입하고 있었습니다. useEffect는 React의 hydration 이후에 실행되므로, 자연스럽게 스크립트는 hydration 이후에 로드됨을 확인할 수 있었습니다.

2. idle

post-hydrate 전략과 거의 동일했지만, requestIdleCallback Web API를 사용해 스크립트를 삽입하고 있었습니다. 이를 통해 메인 스레드가 유휴 상태일 때 스크립트를 로드하도록 되어 있었고, 브라우저에서 해당 API를 지원하지 않는 경우 setTimeout으로 대체하는 폴리필 코드가 있었습니다.

3. off-main-thread

이 전략은 다른 두 방식과는 달리 <script>를 실제로 리턴합니다. 단, Partytown을 통해 동작하도록 type="text/partytown" 속성을 추가하고 props를 적절히 변환하여 렌더링하고 있었습니다.

requestIdleCallback과 Partytown

이 과정에서 requestIdleCallbackPartytown이라는 두 가지 새로운 키워드를 알게 되었고, 이에 대해 간단히 정리했습니다.

requestIdleCallback

requestIdleCallback은 브라우저가 유휴 상태일 때 콜백 함수를 실행하도록 요청하는 API입니다. 이를 통해 스크립트를 메인 스레드에 부담을 주지 않고 로드할 수 있습니다.

Partytown

Partytown은 Web Worker를 활용하여 스크립트를 백그라운드 스레드에서 로드함으로써 메인 스레드의 성능을 최적화하는 라이브러리입니다. 이 라이브러리를 활용해 스크립트를 비동기적으로 처리함으로써 페이지 로딩 속도에 영향을 최소화할 수 있습니다.

결론

구글 애드센스에서 사이트 소유권을 확인할 때처럼, SSR에서 스크립트를 삽입할 필요가 있을 경우에는 Gatsby Script API는 적절하지 않다는 것을 알게 되었습니다. post-hydrate, idle 방식은 SSR 시 <script> 태그를 렌더링하지 않으며, off-main-thread 방식은 Partytown을 사용해 스크립트를 백그라운드에서 proxy URL을 통해 로드하기 때문에 봇이 이를 제대로 감지하지 못할 가능성이 있었습니다.

또한 코드 구현을 확인하는 과정에서 requestIdleCallback과 Partytown이라는 성능 최적화 기술을 새로 알게 되었고, 여기에 대해 좀더 깊이 알아봐야겠다는 생각을 했습니다.

Copyright 2022-2025.hanse-kimAll right reserved.