최신 SPA에 크롤러를 돌리면 페이지 한 개만 돌아오는 경우가 많습니다. 홈 하나, 링크 세 개. 실제 사이트는 400페이지인데도 말입니다. 브라우저에서는 아무 문제가 없습니다. 크롤러가 그 페이지들을 본 적이 없을 뿐입니다.
기술적으로 멀쩡한 사이트가 유기 노출이 거의 없는 가장 흔한 원인이며, 렌더링과 크롤링의 관계를 이해하면 충분히 해결 가능합니다.
두 번의 웨이브 모델
검색엔진은 JavaScript 페이지를 두 단계로 처리합니다.
- 크롤 및 HTML 파싱 — 원본 응답을 받아 그 안의 링크를 추출합니다.
- 렌더링 — 헤드리스 브라우저 큐에 들어가 JavaScript를 실행하고, 결과 DOM을 다시 파싱해 새로 생긴 링크를 추출합니다.
두 번째 웨이브는 즉시 보장되지 않습니다. 렌더링 리소스 여유와 해당 페이지의 가치 판단에 따라 달라집니다. 대규모 사이트에서는 1차 웨이브보다 며칠에서 몇 주 늦고, 우선순위가 낮은 페이지에는 사실상 오지 않을 수도 있습니다.
JavaScript 실행 이후에만 존재하는 모든 것이 이 지연과 불확실성을 그대로 물려받습니다.
클라이언트 렌더링이 발견을 망가뜨리는 지점
링크가 아닌 링크. 가장 큰 문제입니다.
<!-- 1차 크롤에 보이지 않음 -->
<div onclick="router.push('/products/123')">상품 보기</div>
<a href="#" onclick="navigate('/products/123')">상품 보기</a>
<button data-url="/products/123">상품 보기</button>
반면 이렇게 쓰면 즉시 발견됩니다.
<a href="/products/123">상품 보기</a>
라우터 기반 메뉴가 <div>로 렌더링되면 순회 가능한 링크 그래프가 없는 사이트가 됩니다. AttractiveWebAI의 크롤러는 onclick 핸들러와 data-* 속성까지 파싱해 일부를 복구합니다. 유용한 안전망이지만, 검색엔진이 그런 안전망에 의존해야 하는 내비게이션을 만드는 것은 분명한 비용이 따르는 선택입니다.
인터랙션 뒤의 콘텐츠. 탭, 아코디언, '더 보기' 버튼처럼 클릭 시점에 데이터를 가져오는 콘텐츠는 렌더 DOM에도 없는 경우가 많습니다. 크롤러는 클릭하지 않기 때문입니다.
무한 스크롤. 2페이지가 스크롤 이벤트 이후에만 존재한다면 크롤 관점에서는 존재하지 않습니다. 스크롤 경험과 별도로 페이지네이션 URL을 제공해야 합니다.
클라이언트 전용 메타데이터. 마운트 이후 JavaScript가 넣는 title·description은 2차 웨이브에서 잡힐 수 있지만, 소셜 크롤러와 상당수 비구글 엔진은 아예 렌더링하지 않습니다. 공유 링크에 앱 셸이 그대로 노출되는 이유입니다.
렌더링 전략 선택
| 전략 | 색인 신뢰도 | 적합한 대상 |
|---|---|---|
| SSG(정적 생성) | 가장 높음 | 마케팅 페이지, 문서, 블로그 |
| ISR / 온디맨드 재생성 | 높음 | 주기적으로 바뀌는 대규모 카탈로그 |
| SSR(서버 렌더링) | 높음 | 색인이 필요한 개인화·고빈도 변경 페이지 |
| CSR(클라이언트 렌더링) | 낮음 | 대시보드, 로그인 도구 — 애초에 색인 대상이 아닌 페이지 |
판단 기준은 하나입니다. 노출이 필요한 페이지라면 콘텐츠와 링크가 최초 HTML 응답에 있어야 합니다. 나머지는 최적화의 영역입니다.
대부분은 하이브리드가 정답입니다. Next.js라면 마케팅·블로그·카테고리·상품 페이지는 SSG 또는 ISR로, 계정 영역과 대시보드는 CSR로 두고 robots.txt에서 차단합니다. 색인할 생각이 없는 페이지에 서버 렌더링 비용을 지불할 이유는 없습니다.
10분 자가 진단
- 개발자도구가 아니라 페이지 소스 보기.
Ctrl+U는 서버가 보낸 원본 HTML을,검사는 JavaScript 실행 후 DOM을 보여줍니다. 상품 설명이 후자에만 있다면 렌더링 의존입니다. - JavaScript를 끄고 사이트를 탐색해 보세요. 그 상태에서 이동 가능한 범위가 1차 크롤이 보는 범위입니다.
curl로 앵커 수를 세어 봅니다.
브라우저 화면과 무관하게, 홈에서 링크가 3개 나온다면 발견 문제입니다.curl -s https://example.com/ | grep -o 'href="[^"]*"' | wc -l- 크롤 모드를 비교합니다. DeepAnalyzer의 기본 fetch 모드는 원본 HTML을, Strong Mode는 실제 브라우저를 사용합니다. 두 모드의 링크 수·헤딩 수·콘텐츠 점수 차이가 곧 JavaScript 의존도의 정량적 측정값입니다.
특히 마지막 비교는 리플랫폼 의사결정 전에 반드시 해보길 권합니다. "SPA라 SEO가 걱정된다"는 막연한 말을 티켓에 적을 수 있는 숫자로 바꿔 주기 때문입니다.
영향이 큰 순서대로의 해결책
진짜 앵커를 쓰세요. 프레임워크의 <Link> 컴포넌트는 실제 <a href>를 렌더링합니다. 이를 사용하고, 프로그래매틱 내비게이션은 콘텐츠 링크가 아니라 실제 앱 인터랙션에만 씁니다.
색인 대상은 서버 렌더링합니다. 콘텐츠, 헤딩, canonical, title, description, 구조화 데이터는 최초 응답에 있어야 합니다.
모든 뷰에 URL을 부여합니다. 모달 전용·탭 전용·스크롤 전용 콘텐츠는 노출될 대상 자체가 없습니다. 검색 트래픽을 받을 가치가 있다면 주소를 주어야 합니다.
렌더 DOM과 원본 DOM을 일치시킵니다. 클라이언트에서 주입한 canonical이나 noindex가 서버 응답과 충돌하면 결과는 운에 맡겨집니다.
JS·CSS를 차단하지 마세요. robots.txt에서 /static/이나 /_next/를 막으면 렌더링 자체가 불가능해져 빈 셸만 남습니다.
측정에 대하여
렌더링을 수정한 뒤에는 두 가지를 봅니다. 크롤당 발견 URL 수, 그리고 Search Console의 '크롤됨 – 현재 색인되지 않음' 비율입니다. 렌더링 수정은 보통 발견 수 급증으로 먼저 나타나고, 2~4주 뒤 색인 개선으로 이어집니다. 발견 수가 움직이지 않는다면 수정이 의도한 지점에 적용되지 않은 것이므로, 브라우저가 아니라 원본 HTML을 다시 확인하세요.
자주 묻는 질문
구글은 JavaScript를 렌더링하나요?
합니다. Googlebot은 최신 Chromium으로 렌더링합니다. 다만 렌더링은 큐에 들어가고 리소스 제한이 있어 크롤보다 늦습니다. Bing·네이버·바이두와 대부분의 소셜 미리보기 크롤러는 렌더링이 불안정하거나 아예 하지 않습니다. 서버 렌더링 콘텐츠는 어디서나 동일하게 처리되지만, 클라이언트 렌더링 콘텐츠는 그렇지 않습니다.
React나 Next.js는 SEO에 불리한가요?
아닙니다. 프레임워크는 중립이고 중요한 건 렌더링 모드입니다. 공개 페이지를 SSG나 SSR로 제공하는 Next.js 앱은 SEO에 매우 유리합니다. 같은 앱을 순수 CSR로 배포하면 그렇지 않습니다.
프리렌더링 서비스로 해결되나요?
과도기 대안으로는 가능합니다. 크롤러에는 정적 스냅샷을, 사용자에게는 앱을 제공합니다. 다만 실제 사이트와 어긋나는 두 번째 파이프라인이 생기고, 크롤러에 실질적으로 다른 콘텐츠를 제공할 위험이 있습니다. 코드베이스를 직접 통제할 수 있다면 네이티브 SSG/SSR이 낫습니다.
무한 스크롤은 어떻게 처리하나요?
스크롤 경험은 유지하되, 서버 렌더링되는 페이지네이션 URL(?page=2)을 목록에서 링크로 함께 제공하세요. 사용자는 스크롤을, 크롤러는 주소를 얻습니다.
탭 안에 있는 콘텐츠는 문제인가요?
클릭 시점에 가져오는 경우에만 문제입니다. HTML에 이미 존재하고 CSS로만 숨겨진 콘텐츠는 대체로 정상 색인됩니다. 클릭이 네트워크 요청을 유발하는 구조라면 색인되지 않습니다.