React나 Vue 같은 현대적인 프레임워크로 웹 애플리케이션을 구축했지만 구글이나 네이버 검색 엔진에 내 페이지가 전혀 노출되지 않는 문제를 경험해봤을 것이다.
다만 클라이언트 사이드 렌더링(CSR) 특성상 검색 엔진 크롤러가 자바스크립트를 실행하기 전 빈 HTML만 수집한다는 정확한 원인이나 해결책을 모르는 경우가 많다.
이번에는 CSR 기반 SPA에서 발생하는 검색 색인 누락 원인이 정확히 뭔지, Dynamic Rendering이 왜 필요한지, Nginx 서버 환경에서 어떻게 구현하는지 완벽하게 정리해서 소개하겠다.
React, Vue, Angular로 제작된 Single Page Application(SPA)은 최초 접속 시 비어 있는 HTML 틀만 다운로드하고, 브라우저가 자바스크립트 번들 파일을 실행하면서 동적으로 화면(DOM)을 그린다.
문제는 검색 엔진 크롤러의 동작 방식이다. 구글봇(Googlebot)은 자바스크립트 실행 능력이 일부 개선되었지만, 대량의 페이지를 수집할 때 렌더링 큐에 들어가면서 색인이 수일에서 수주일 뒤로 밀리거나 자바스크립트 실행 전 상태인 빈 페이지로 수집하는 경우가 빈번하다. 특히 네이버 예티(Yeti)나 기타 수집 로봇은 자바스크립트 해석 능력이 매우 제한적이어서 빈 태그 외에는 아무런 본문 텍스트도 인식하지 못한다.
기존 CSR 구조의 장점인 빠른 사용자 경험을 유지하면서 검색 노출 문제를 해결하기 위해 여러 렌더링 기법을 고려할 수 있다.
| 구분 | SSR (Server-Side Rendering) | SSG (Static Site Generation) | Dynamic Rendering |
|---|---|---|---|
| 동작 방식 | 요청 시마다 Node.js 서버에서 HTML 생성 | 빌드 시점에 정적 HTML 파일 미리 생성 | 일반 사용자에겐 CSR, 크롤러에겐 Pre-render HTML 제공 |
| 전환 난이도 | 기존 프론트엔드 코드 구조 전체 재작성 필요 | 블로그나 정적 콘텐츠 사이트에만 한정적 | 기존 SPA 소스 코드 수정 없이 서버 프록시 설정만으로 적용 |
| SEO 이점 | 매우 높음 | 매우 높음 | 매우 높음 (크롤러 타겟팅) |
| 서버 부하 | 높음 (모든 요청 처리) | 낮음 (정적 파일 제공) | 낮음 (크롤러 요청 시에만 서버 렌der 실행) |
Dynamic Rendering은 요청을 보내는 클라이언트의 User-Agent를 서버에서 확인하여, 일반 사용자라면 기존 CSR 정적 파일을 리턴하고 검색 크롤러라면 렌더링 엔진(Puppeteer나 Prerender 서비스)을 거친 완제품 HTML을 리턴하는 방식이다.
✗ 검색 로봇을 별도로 구분하지 않고 단순 SPA 라우팅 설정만 적용해둔 상태다. 이 경우 검색 로봇은 데이터가 비어 있는 `
` 형태의 HTML만 읽어간 뒤 색인을 포기한다.# ✗ 잘못된 설정: 검색 로봇 분기 없이 index.html만 렌더링
server {
listen 80;
server_name example.com;
location / {
root /var/www/html;
try_files $uri $uri/ /index.html;
}
}
✓ 검색 로봇 식별 목록을 작성한 뒤, 조건에 해당하는 요청만 헤드리스 브라우저 렌더링 포트(또는 외부 Prerender 미들웨어)로 우회 처리한다.
# ✓ 올바른 설정: 검색 크롤러 접속 시 Dynamic Rendering 서버로 미러링
server {
listen 80;
server_name example.com;
location / {
set $prerender 0;
# 대표적인 검색 엔진 크롤러 User-Agent 식별
if ($http_user_agent ~* "googlebot|bingbot|yandex|baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|naverbot|yeti") {
set $prerender 1;
}
# 정적 리소스(이미지, JS, CSS) 요청은 렌더링 대상에서 제외
if ($uri ~* "\.(js|css|xml|less|png|jpg|jpeg|gif|pdf|doc|txt|ico|rss|zip|mp3|rar|exe|wmv|doc|avi|ppt|mpg|mpeg|tif|wav|mov|psd|ai|xls|mp4|m4a|swf|dat|dmg|iso|flv|m4v|torrent|ttf|woff|svg|eot)") {
set $prerender 0;
}
# 크롤러 요청인 경우 3000번 포트의 Render 미들웨어로 요청 전달
if ($prerender = 1) {
rewrite .* /render?url=https://$host$request_uri break;
proxy_pass http://127.0.0.1:3000;
}
# 일반 사용자 요청은 기존대로 CSR 처리
root /var/www/html;
try_files $uri $uri/ /index.html;
}
}결과/출력값:
Googlebot 및 네이버 Yeti 수집기가 사이트에 접속하면 Nginx가 이를 감지하여 Puppeteer 기반 렌더링 서버(3000번 포트)로 페이지를 요청한다. 렌더링 서버가 자바스크립트를 모두 실행한 완료 상태의 HTML을 크롤러에게 전달하므로, 검색 엔진은 본문 텍스트와 구조화 데이터를 완벽하게 색인한다. 일반 브라우저 접속자는 기존 CSR 상태 그대로 웹사이트를 이용하므로 빠른 화면 전환 속도가 보존된다.
편의를 위해 크롤러용 페이지를 별도로 구성할 때 주의하지 않으면 검색 엔진 제재를 받을 수 있다.
✗ 잘못된 것: 클로킹(Cloaking) 행위.
크롤러에게 보여주는 HTML 콘텐츠와 일반 사용자 브라우저에 노출되는 실제 화면의 내용이나 텍스트를 완전히 다르게 작성하는 것이다. 검색 엔진은 이를 유저를 속이는 스팸 행위로 규정하여 검색 결과에서 완전히 영구 제외(페널티)시킨다.
✓ 올바른 것: 동일성 유지.
Prerender 엔진이 리턴하는 HTML 텍스트는 브라우저에서 자바스크립트 실행이 끝난 후 DOM에 남아있는 텍스트 content 및 og 태그, canonical 태그와 완전히 일치해야 한다.
React/Vue SPA의 검색 노출 누락 방지는 서비스 성장의 필수적인 요소다. 렌더링 방식에 대한 작은 최적화와 서버 단의 조건부 프록시 환경 구성이 모여서 검색 유입 극대화라는 큰 효과를 만든다는 점을 잊지 말자. 이 글의 Nginx 조건부 프록시 설정 부분을 참고해 서버 환경에 Dynamic Rendering을 적용해두면, 기존 프론트엔드 소스 코드를 Next.js나 Nuxt.js로 완전히 뒤엎지 않고도 완벽한 검색 엔진 색인을 얻을 수 있을 것이다.