🧩

JavaScript SEO와 렌더링

Google이 JavaScript를 렌더링하는 방식 — 그리고 SEO를 위해 SSG, SSR, ISR, 동적 렌더링 중 무엇을 선택할지.

📖 12 분 읽기 🕑 업데이트 2026-06-22

현대 프런트엔드 프레임워크는 대부분의 콘텐츠를 브라우저에서 실행되는 JavaScript로 전달합니다. 이는 제품 엔지니어링 측면에서는 환상적이지만, 크롤러가 HTML을 내려받아 읽는다는 단순한 SEO의 멘탈 모델에는 끔찍합니다. Google은 실제로 JavaScript를 실행하기는 합니다 — 하지만 자신만의 타임라인, 자신만의 예산, 그리고 조용히 순위를 갉아먹는 실패 모드를 가지고 그렇게 합니다. 가장 중요한 콘텐츠가 fetch()가 해결된 뒤에만 존재한다면, 당신은 며칠 동안 지연되거나 기대한 방식으로 결코 완료되지 않을 수도 있는 렌더에 자연 유입 트래픽을 걸고 있는 셈입니다.

이 가이드는 Google의 렌더링 파이프라인 안에서 실제로 무슨 일이 일어나는지 설명하고, 빌드 시점에 선택할 수 있는 렌더링 전략들을 비교하며, 실제 앱을 물어뜯는 함정들을 짚고, 오늘 바로 실행할 수 있는 디버깅 워크플로를 제시합니다.

Google이 렌더링하는 방식

색인 생성을 단일 패스가 아니라 두 단계(two-wave) 프로세스로 생각하면 도움이 됩니다.

  1. 크롤(Crawl). Googlebot이 URL을 요청하고 초기 HTML 을 돌려받습니다 — 정확히 curl https://example.com이 반환할 그것입니다. 여기서 발견된 링크는 크롤 프런티어로 들어갑니다. 초기 HTML이 빈 <div id="root"></div>라면, 이 단계는 거의 아무것도 보지 못합니다.
  2. 렌더(Render). URL이 Web Rendering Service(WRS) 큐에 들어갑니다. WRS는 헤드리스, 에버그린 Chromium입니다. 용량이 확보되면 페이지를 로드하고, JavaScript를 실행하고, 네트워크가 안정될 때까지 기다린 다음, 렌더링된 HTML(JavaScript 실행 이후의 DOM)을 만들어냅니다.
  3. 색인(Index). Google은 렌더링된 HTML을 색인하고, 거기서 발견한 새로운 링크를 다음 크롤 사이클을 위해 추출합니다.

핵심 통찰은 1단계와 2단계 사이의 간극입니다. JavaScript 실행은 HTML을 가져오는 것보다 대략 한 자릿수(order of magnitude) 더 비싸기 때문에 렌더링은 뒤로 미뤄집니다. 실제로 그 지연은 보통 몇 분이지만, 크롤 수요가 높거나 사이트의 렌더 큐가 클 때는 며칠까지 늘어날 수 있습니다.

              CRAWL                  RENDER (WRS queue)            INDEX
  Googlebot ──► raw HTML ──► [ wait: minutes → days ] ──► headless
                  │                                        Chromium
                  ▼                                           │
            links found                                  rendered DOM
            in raw HTML                                       │
                  └──────────────► next crawl cycle ◄── new links found

이 지연은 당신이 직접 통제할 수 없는 두 가지 예산과 상호작용합니다.

  • 크롤 예산(Crawl budget) — 특정 시간 창 안에서 Googlebot이 당신의 오리진에서 가져오는 URL의 수. 사이트 권위도, 서버 속도, 오류율에 의해 좌우됩니다.
  • 렌더 예산(Render budget) — 대략, WRS가 당신의 페이지를 렌더링하는 데 기꺼이 쓰려는 CPU/시간의 양. 느리고 스크립트가 무거운 페이지는 더 많이 소비하므로, 사이클당 더 적은 수의 페이지만 렌더링됩니다.

⚠️ 주의: 전형적인 실패는 클라이언트 사이드 주입 이후에만 존재하는 콘텐츠입니다. 크롤 단계는 빈 껍데기를 보고, 렌더 단계가 지연되거나 WRS 하에서 스크립트가 오류를 일으키면 콘텐츠는 결코 색인되지 않습니다. “내 브라우저에서는 잘 돼”는 Googlebot에게도 잘 된다는 증거가 아닙니다 — 당신의 브라우저는 속도 제한이 걸려 있지도, 샌드박스화되어 있지도, 몇 주 묵은 캐시 응답을 실행하고 있지도 않습니다.

개발자를 놀라게 하는 WRS의 몇 가지 구체적 특성:

  • WRS는 스크롤하지 않고, 클릭하지 않으며, 마우스를 움직이지 않습니다. 사용자 상호작용 뒤에 가려진 것은 무엇이든 WRS에게 보이지 않습니다.
  • WRS는 권한 요청(위치 정보, 카메라, 알림)을 자동으로 거부합니다. 그런 프롬프트를 기다리는 코드는 멈춰버립니다.
  • WRS는 리소스(JS, CSS, API 응답)를 자체 일정에 따라 공격적으로 캐싱하는데, 그 기간은 당신의 HTTP 캐시 헤더가 시사하는 것보다 훨씬 길 수 있습니다. “방금 배포한 최신본”이 오래된 에셋을 기준으로 렌더링될 수 있습니다.
  • WRS는 최신 Chromium을 사용하므로 현대적인 JS 문법은 문제없습니다 — 하지만 렌더 간에 믿고 의존할 수 있는 localStorage/sessionStorage 영속성은 없고, 쿠키도 로그인한 사용자처럼 운반되지 않습니다.

렌더링 전략

HTML을 어디에서 생성하느냐가 이 위험을 얼마나 짊어질지를 결정합니다. 다음은 실제로 그 사이에서 선택하게 될 다섯 가지 전략입니다.

전략HTML이 생성되는 시점크롤러가 콘텐츠를 즉시 보는가?SEO 친화성가장 적합한 용도
CSR (클라이언트 사이드 렌더)브라우저에서, JS 실행 후아니오 — 먼저 빈 껍데기⚠️ 위험로그인 대시보드, 인증 뒤의 앱형 UI
SSR (서버 사이드 렌더)요청마다, 서버에서✅ 강력개인화되거나 자주 바뀌는 공개 페이지
SSG (정적 사이트 생성)빌드 시점에✅✅ 가장 강력문서, 블로그, 마케팅, 안정적인 모든 것
ISR (증분 정적 재생성)빌드 시 생성 후, 일정/온디맨드로 재생성예 (마지막 정상 정적본을 제공)✅✅ 강력변하지만 요청마다는 아닌 대형 카탈로그
동적 렌더링요청마다, 봇은 사전 렌더링된 HTML, 사용자는 CSR예 (봇에 한해)⚠️ 임시방편다시 쓸 수 없는 레거시 SPA

표가 담아낼 수 없는 몇 가지 메모:

  • CSR은 순수한 create-react-app이나 설정되지 않은 SPA의 기본값입니다. 이 가이드에 나오는 거의 모든 JavaScript-SEO 문제를 만들어내는 전략입니다. 색인할 것이 아무것도 없는 로그인 뒤 에서는 완벽히 괜찮습니다.
  • SSR은 첫 요청에 완전히 구성된 HTML을 보낸 다음, 이를 인터랙티브 앱으로 “하이드레이션(hydrate)“합니다. 크롤러가 1단계에서 실제 콘텐츠를 받으므로 렌더링 지연이 더는 문제되지 않습니다. 대가는 요청마다 발생하는 서버 연산과 지연 시간입니다.
  • SSG는 빌드 시점에 모든 페이지를 정적 .html 파일로 사전 렌더링합니다. 기다릴 렌더도 없고 느려질 서버도 없습니다 — 크롤러는 CDN이 제공하는 바로 그 파일을 읽습니다. 콘텐츠 사이트의 황금 표준입니다.
  • ISR은 갱신될 수 있는 SSG입니다: 페이지는 정적이지만, N초 후 또는 온디맨드로 백그라운드에서 재생성될 수 있습니다(예: Next.js revalidate). 크롤러는 항상 유효한 정적 페이지를 받으며, 신선도만 약간 늦습니다. 제품 페이지가 50,000개이고 가격이 바뀔 때마다 전체를 재빌드하는 것이 불가능할 때 이상적입니다.
  • 동적 렌더링은 사용자 에이전트를 탐지해 봇에게는 사전 렌더링된 스냅샷(Prerender.io나 Puppeteer 같은 것을 통해)을 제공하고, 실제 사용자에게는 SPA를 제공합니다. Google은 이제 이를 권장 사항이 아니라 임시방편(workaround) 이라고 부릅니다 — 취약한 UA 탐지 계층과 유지보수해야 할 두 번째 렌더링 파이프라인을 추가하기 때문입니다. 앱을 다시 쓰는 것이 불가능할 때만 손을 뻗으세요.

🧑‍💻 개발자 관점: 이를 누가 렌더링 비용을 치르는가 의 문제로 생각하세요. CSR은 사용자의 기기와 Google의 WRS가 (두 번) 치르게 합니다. SSR은 당신의 서버가 매 요청마다 치르게 합니다. SSG는 빌드 시점에 한 번 치르고 다시는 치르지 않습니다. 그 비용을 더 왼쪽으로 — 빌드 시점 쪽으로 — 밀어붙일수록, Google의 렌더 예산을 덜 쓰고 렌더가 실패할 수 있는 경로도 줄어듭니다.

함정

다음은 실제 감사에서 드러나는 문제들로, 조용한 트래픽 손실을 일으키는 빈도순으로 대략 정렬했습니다.

1. <head>를 결코 갱신하지 않는 클라이언트 사이드 라우팅. SPA에서 라우트 간 이동은 전체 페이지 로드 없이 DOM을 교체합니다. 라우터가 title, meta description, canonical까지 갱신하지 않으면, 모든 라우트가 초기 HTML에 있던 값을 그대로 물려받습니다. 결국 Google은 모두 자기가 홈페이지라고 주장하는 열 개의 페이지를 색인하게 됩니다.

// ❌ Title and canonical are stale on every soft navigation
router.push('/pricing'); // DOM changes, <head> does not

// ✅ Update head metadata on every route change
router.afterEach((to) => {
  document.title = to.meta.title;
  document
    .querySelector('link[rel="canonical"]')
    .setAttribute('href', `https://example.com${to.path}`);
});

2. 스크롤이나 상호작용을 요구하는 지연 로드(lazy-load) 콘텐츠. 무한 스크롤, “더 보기” 버튼, IntersectionObserver로 마운트되는 콘텐츠는 WRS에게 보이지 않습니다 — 스크롤하지도 클릭하지도 않기 때문입니다. 기사 본문이나 제품 목록이 스크롤 이벤트 이후에만 나타난다면, 그것은 Google이 색인하는 렌더링된 DOM에 들어 있지 않습니다.

3. 해시 기반 라우팅. example.com/#/products/42 같은 URL은 옛 SPA의 유물입니다. # 뒤의 모든 것은 프래그먼트(fragment) 로, 명세상 서버로 전혀 전송되지 않으며 Google은 이를 example.com/동일한 URL 로 취급합니다. 해시 라우트는 사이트 전체를 색인 가능한 단 하나의 페이지로 붕괴시킵니다. 라우트가 실제로 서버에서 해석 가능한 경로가 되도록 항상 History API(pushState)를 사용하세요.

4. 타임아웃과 렌더 차단 작업. WRS는 너무 오래 걸리는 렌더를 포기합니다. 의존성이 연쇄된 fetch() 호출의 워터폴, 4 MB 번들, 멈춰버린 서드파티 스크립트는 콘텐츠를 컷오프 너머로 밀어낼 수 있습니다. 느린 렌더는 렌더 예산도 태우므로, 전체적으로 더 적은 수의 페이지만 렌더링됩니다.

5. 사용자 상호작용이나 동의 뒤에 가려진 콘텐츠. “수락”을 누를 때까지 렌더링을 막는 쿠키 동의 벽, 클릭해야만 콘텐츠가 로드되는 탭, “더 읽기” 버튼으로 드러나는 본문 — WRS는 이들 중 무엇도 클릭하지 않습니다. SEO에 중요하다면 상호작용 없이 DOM에 들어 있어야 합니다.

6. 소프트 404와 JS 기반 오류 상태. SPA가 리소스를 찾지 못하면 종종 “찾을 수 없음” 메시지를 렌더링하면서도 HTTP 200을 반환합니다. Google은 빈약한 콘텐츠를 담은 성공 응답을 보고 그 오류 페이지를 색인할 수 있습니다. 서버에서 실제 404/410 상태를 반환하거나, 정말로 없는 콘텐츠에는 noindex를 사용하세요.

모범 사례

관통하는 원칙: 중요한 콘텐츠와 메타데이터를 JavaScript 실행 전에 존재하는 HTML에 넣고, JavaScript는 향상(enhancement)으로 취급하라.

  • 중요한 콘텐츠를 서버 사이드 또는 빌드 시점에 렌더링하라. 헤드라인, 본문, 주 내비게이션, 제품 상세, 가격은 SSR이나 SSG를 통해 초기 HTML에 들어가야 합니다. JavaScript가 페이지를 만들게 하지 말고, 이미 완성된 페이지를 향상시키게 하세요.
  • 점진적 향상(progressive enhancement)을 실천하라. 페이지는 JS가 비활성화된 상태에서도 핵심 의미를 전달해야 합니다. 링크는 실제 <a href> 요소여야 합니다(WRS는 <a href>를 따라가지, router.push를 호출하는 onClick 핸들러를 따라가지 않습니다). 이동시키는 버튼은 링크여야 합니다.
  • History API를 사용하고 라우트마다 메타데이터를 갱신하라. 모든 라우트는 자체 title, meta description, 자기 참조 canonical을 가져야 하며, 이동 시마다 갱신되어야 합니다. 프레임워크의 head 관리자(react-helmet-async, @unhead/vue, Next/Astro의 내장 기능)가 이를 깔끔하게 처리합니다.
// Per-route head with a framework-agnostic shape
function setSeoHead({ title, description, canonical }) {
  document.title = title;
  upsertMeta('description', description);
  upsertLink('canonical', canonical);
}
  • 구조화 데이터를 지연된 fetch 뒤가 아니라 HTML에 넣어라. JSON-LD는 가능한 한 이른 시점에 렌더링된 DOM에 존재해야 합니다. SSG/SSR은 이를 직접 내보냅니다; 클라이언트 사이드에서 주입해야만 한다면 마운트 시 동기적으로 하고, 비동기 데이터 왕복 이후에는 절대 하지 마세요.
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "JavaScript SEO & Rendering",
  "datePublished": "2026-06-22"
}
</script>
  • JS를 가볍고 견고하게 유지하라. 코드 스플리팅을 하고, 비핵심 스크립트는 지연시키며, 단 하나의 서드파티 실패가 페이지를 비워버리지 않도록 하세요. 분석 도구와 태그 매니저는 콘텐츠를 절대 막지 않도록 셀프 호스팅하거나 async로 두세요.
  • robots.txt에서 JS/CSS를 차단하지 마라. WRS는 페이지를 렌더링하려면 당신의 스크립트와 스타일을 가져와야 합니다. /_next//assets/를 차단하면 Google은 깨진 페이지를 렌더링하게 됩니다.

💡 팁: 어떤 페이지든 빠르게 점검하려면 — curl -s https://your-url | grep "your headline text"를 실행하세요. 주요 콘텐츠가 그 출력에 없다면, 크롤 단계는 그것을 볼 수 없고 당신은 전적으로 지연된 렌더 단계에 의존하고 있는 것입니다. 그것이 바로 SSG/SSR(콘텐츠 존재)과 CSR(콘텐츠 부재)을 가르는 선입니다.

디버깅

Google이 무엇을 보는지 추측할 필요가 없습니다 — Google의 도구가 렌더링된 출력을 직접 보여줍니다.

  • Google Search Console의 URL 검사. 라이브 URL을 검사한 다음 테스트한 페이지 보기 → HTML을 여세요. 이것이 WRS가 생성한 렌더링된 HTML입니다. 여기서 헤드라인, canonical, JSON-LD를 검색해 보세요. 여기에 없다면 색인에도 없는 것입니다. 스크린샷 탭과 추가 정보 → 페이지 리소스 탭은 렌더를 망가뜨린 차단된 스크립트와 콘솔 오류를 드러냅니다.
  • site: 연산자. site:example.com/pricing 또는 site:example.com "페이지의 정확한 문구"는 어떤 페이지(그리고 JS로 주입된 특정 텍스트)가 실제로 색인되었는지 알려줍니다. 렌더링된 DOM에만 있는 문구에 결과가 없다면, 렌더가 안착하지 않고 있다는 강력한 신호입니다.
  • 원본 DOM과 렌더링된 DOM을 직접 비교하라. view-source:(또는 curl)는 크롤 단계의 HTML을 보여줍니다. DevTools → Elements는 JS 실행 이후의 DOM을 보여줍니다. 둘 사이의 차이(diff) 가 바로 성공적인 렌더에 의존하는 콘텐츠입니다 — SEO에 중요한 것이라면 그 차이를 최소화하세요.
  • Googlebot의 제약을 시뮬레이션하라. Chrome DevTools에서 JavaScript를 비활성화(Command Menu → “Disable JavaScript”)하고 새로고침하세요: 이는 크롤 단계를 근사합니다. 네트워크와 CPU를 스로틀링하면 WRS 부하 하에서 타임아웃될 렌더가 드러납니다. 무료 Rich Results TestMobile-Friendly Test 역시 Google 자체 엔진으로 렌더링하고 렌더링 오류를 보고합니다.

⚠️ 주의: 수정 사항을 검증할 때는 Google의 캐시 버전이 아니라 항상 라이브 URL을 테스트하세요 — 캐시는 마지막으로 성공한 렌더를 반영하며, 그 시점이 당신의 배포보다 며칠 앞설 수 있습니다.

여기서 더 나아가기

JavaScript 렌더링은 근본적으로 당신이 빌드 계층(build layer) 에서 내리는 결정입니다 — 선택하는 프레임워크와 출력 모드가 애초에 이런 문제들을 마주하게 될지 여부를 결정합니다. 그 트레이드오프는 빌드 계층 가이드에서 깊이 파고드세요.

바로 이 사이트는 Astro와 정적 생성을 사용해 이 문제 범주 전체를 비껴갑니다: 모든 페이지가 빌드 시점에 HTML로 사전 렌더링되므로, 크롤 단계가 완전한 콘텐츠, 구조화 데이터, 메타데이터를 지연된 렌더 큐에 전혀 의존하지 않고 봅니다. 이는 우연이 아닙니다 — 사용할 수 있는 가장 SEO 친화적인 기본값이며, 콘텐츠 사이트가 SSG를 먼저 집어 들어야 하는 이유입니다.

핵심 요약

  • ✅ Google은 JavaScript를 지연된 두 번째 단계(WRS)에서 렌더링합니다; 주입된 콘텐츠가 신속히 — 또는 아예 — 색인될 것이라고 절대 가정하지 마세요.
  • ✅ 렌더링 비용을 빌드 시점 쪽으로 밀어붙이세요: SSG가 가장 강력하고, SSR이 강력하며, CSR은 색인하고 싶은 무엇에든 위험합니다.
  • ✅ 모든 클라이언트 사이드 라우트 변경 시 title, meta description, 자기 참조 canonical을 갱신하세요.
  • ✅ 중요한 콘텐츠와 JSON-LD를 JS 실행 전에 존재하는 HTML에 두세요; WRS는 스크롤하지도, 클릭하지도, 권한을 부여하지도 않습니다.
  • ✅ GSC URL 검사(렌더링된 HTML), site: 연산자, JS 비활성화 새로고침으로 검증하세요 — 캐시가 아니라 라이브 URL을 테스트하세요.