리눅스·서버 학습

React 사이트가 검색에 안 나올 때: 빈 HTML 문제 해결기

모든 페이지가 같은 제목의 빈 화면으로 수집되던 문제를 찾고 빌드 단계에서 해결한 기록입니다.

이 사이트는 React 단일 페이지 앱입니다. 화면에서는 페이지마다 제목과 내용이 잘 보였지만, 구글 애드센스는 "게시자 콘텐츠가 없는 화면"이라고 판정했고 검색에서 발견된 페이지도 5개뿐이었습니다. 브라우저로만 확인해서는 원인이 보이지 않았습니다.

크롤러가 받는 화면 확인하기

브라우저는 자바스크립트를 실행한 뒤의 화면을 보여줍니다. 원본 HTML을 보려면 curl로 직접 받아야 합니다.

curl -s https://example.com/tools/subnet | grep -o "<title>[^<]*"
curl -s https://example.com/tools/subnet | wc -c

결과는 충격적이었습니다. 어떤 주소를 요청해도 제목이 같았고, 본문이 들어가는 root 요소는 비어 있었습니다. 구글은 자바스크립트를 실행해 읽기도 하지만 시간이 더 걸리고, 다른 크롤러가 어떻게 처리하는지는 보장되지 않습니다.

선택지 비교

  • SSR(서버 렌더링): 가장 정확하지만 앱 구조를 크게 바꿔야 하고 서버 부담이 늘어납니다.
  • 동적 렌더링: 크롤러에게만 다른 화면을 주는 방식이라 관리가 번거롭습니다.
  • 빌드 시 사전 렌더링: 내용이 자주 바뀌지 않는 공개 페이지에 적합하고 서버 비용이 들지 않습니다.

공개 페이지 내용이 대부분 고정되어 있어서 세 번째를 골랐습니다. 빌드가 끝난 뒤 스크립트가 페이지마다 제목, 설명, canonical, 본문을 채운 HTML 파일을 만듭니다.

시험에서 잡은 실수

처음에는 tools/json/index.html처럼 폴더로 만들었는데, nginx가 주소 끝에 슬래시를 붙이며 전 페이지를 301로 보냈습니다. canonical 주소와 어긋나는 문제라서 tools/json.html 파일로 바꾸고 try_files에 $uri.html을 추가했습니다.

location / {
    try_files $uri $uri.html $uri/ /index.html;
}

또 하나는 HTML 캐시입니다. 정적 파일로 나간 HTML이 브라우저에 오래 남으면 이미 사라진 자산 파일을 찾을 수 있어서, 응답 형식이 HTML일 때만 no-cache를 붙였습니다.

map $sent_http_content_type $html_cache_control {
    ~^text/html "no-cache";
    default     "";
}

라이브러리 호환성도 확인하기

react-helmet-async 2.0.5는 React 19를 공식 지원하지 않아서 화면을 옮겨도 제목이 바뀌지 않았습니다. 3.0으로 올리자 이번에는 정적 태그와 새 태그가 함께 남아 canonical이 두 개가 됐습니다. 화면을 그린 뒤 정적 태그를 지워 해결했습니다.

배운 점

  • SEO 점검은 브라우저가 아니라 curl로 시작합니다.
  • 설정은 운영에 올리기 전에 격리된 임시 서버로 시험합니다.
  • 제목이 바뀌는지는 화면 이동까지 포함해 확인합니다.

정규식 테스트