SEO를 처음부터 생각했어야 했다 — 뒤늦은 최적화 분투기

 


사이트를 공개한 지 2주가 지났는데 구글 검색 유입이 0이었다. 알고 보니 구글 봇 눈에는 내 사이트가 텅 빈 HTML 파일이었다. React SPA의 근본적 한계를 그때서야 깨달았다.

검색 유입 제로의 충격

프로젝트를 GitHub Pages에 올리고 나서, 매일 구글 서치 콘솔을 확인했다. 노출 0, 클릭 0. 일주일이 지나도 변함없었다. "아직 인덱싱이 안 됐나 보다"라고 자위했지만, 2주째에 현실을 직시했다.

URL 검사 도구로 확인해보니, 구글이 크롤링한 페이지의 렌더링 결과가 거의 비어 있었다. 타이틀과 간단한 로딩 스피너만 보이고, 실제 콘텐츠는 없었다. 이유는 단순했다. React SPA는 JavaScript가 실행되어야 콘텐츠가 렌더링되는데, 구글 봇이 항상 JavaScript를 완벽하게 실행하는 건 아니기 때문이다.

SPA의 근본적 SEO 문제

전통적인 웹사이트는 서버에서 HTML을 완성해서 보내준다. 구글 봇이 페이지를 요청하면 콘텐츠가 담긴 HTML을 받는다. 하지만 React SPA는 다르다. 서버가 보내는 HTML은 빈 껍데기이고, 브라우저에서 JavaScript가 실행되면서 비로소 콘텐츠가 채워진다.

구글은 JavaScript를 렌더링할 수 있다고 공식적으로 밝히고 있지만, 실제로는 두 단계 인덱싱 과정을 거친다. 첫 번째 단계에서 HTML만 보고, 두 번째 단계에서 JavaScript를 렌더링한다. 두 번째 단계까지 가는 데 시간이 걸리고, 모든 페이지가 완벽하게 렌더링되는 것도 아니다.

Next.js나 Remix 같은 프레임워크를 썼다면 서버 사이드 렌더링(SSR)으로 이 문제를 자연스럽게 해결할 수 있었다. 하지만 이 프로젝트는 Vite로 시작했고, 지금 와서 프레임워크를 바꾸는 건 사실상 처음부터 다시 만드는 것과 같았다.

프리렌더링이라는 절충안

SSR을 도입할 수 없다면, 대안은 프리렌더링이다. 빌드 시점에 각 페이지의 HTML을 미리 생성해두는 방식이다. 구글 봇이 페이지를 요청하면 콘텐츠가 포함된 정적 HTML을 받게 된다.

원리는 이렇다. 빌드가 완료된 후, 헤드리스 브라우저가 각 페이지를 방문해서 JavaScript를 실행하고, 렌더링된 결과 HTML을 파일로 저장한다. 이 HTML 파일을 배포하면, 구글 봇은 JavaScript 실행 없이도 콘텐츠를 읽을 수 있다.

문제는 페이지가 많아질수록 빌드 시간이 길어진다는 것이다. 가이드 페이지, 칼럼, 백과사전 등 콘텐츠를 추가하면서 프리렌더링 대상 페이지가 수십 개로 늘어났고, 빌드 시간도 함께 늘어났다. 그래도 SSR 서버를 운영하는 것보다는 훨씬 단순하다.

react-helmet-async로 동적 메타 태그

프리렌더링으로 HTML 본문 문제는 해결했지만, 메타 태그 문제가 남아 있었다. 모든 페이지의 타이틀과 description이 동일했다. 구글 검색 결과에 표시되는 제목과 설명이 모든 페이지에서 같다면, 사용자는 어떤 페이지를 클릭해야 할지 판단할 수 없다.

react-helmet-async를 도입해서 각 페이지별로 고유한 타이틀, description, og:image 등을 설정했다. 카드 상세 페이지라면 "타로 마스터 | 바보(The Fool) 카드 해석"처럼, 페이지의 내용을 정확히 반영하는 메타 태그를 넣었다.

Open Graph 태그와 Twitter 카드 태그도 추가했다. 소셜 미디어에서 링크를 공유할 때 미리보기가 제대로 표시되도록 하는 것이다. 직접적인 SEO 요소는 아니지만, 소셜 공유로 인한 유입도 무시할 수 없다.

사이트맵 자동 생성

구글이 내 사이트의 모든 페이지를 발견하려면 사이트맵이 필요하다. 수동으로 관리하는 건 페이지가 추가될 때마다 잊어버리기 쉬우니, 빌드 과정에서 자동으로 생성하는 스크립트를 만들었다.

라우트 설정 파일에서 모든 경로를 추출하고, 동적 경로(카드 상세 페이지 등)의 파라미터를 채워서, 전체 URL 목록이 담긴 sitemap.xml을 생성한다. 빌드할 때마다 최신 상태의 사이트맵이 자동으로 만들어진다.

robots.txt에 사이트맵 위치를 명시하고, 구글 서치 콘솔에도 사이트맵을 제출했다. 사이트맵 제출 후 며칠 만에 인덱싱이 눈에 띄게 빨라졌다.

검색 엔진 등록: 구글 그리고 네이버

한국어 콘텐츠라면 구글만으로는 부족하다. 네이버 검색에서의 노출도 중요하다. 네이버 서치어드바이저에 사이트를 등록하고 소유권을 인증했다.

네이버의 인덱싱은 구글과 다른 특성이 있다. 네이버 블로그나 카페 콘텐츠에 비해 외부 웹사이트의 노출 순위가 낮은 편이고, 인덱싱 속도도 느리다. 그래도 등록하지 않으면 아예 노출되지 않으니, 기본은 해두는 것이 맞다.

구글 서치 콘솔에서는 URL 검사 도구가 유용했다. 특정 페이지가 제대로 인덱싱되었는지, 모바일 사용성에 문제가 없는지 바로 확인할 수 있다. 프리렌더링 적용 후 다시 URL 검사를 해보니, 이번에는 콘텐츠가 온전하게 표시되었다.

구조화된 데이터: JSON-LD 스키마 마크업

검색 결과에서 더 눈에 띄려면 구조화된 데이터가 필요하다. JSON-LD 형식의 스키마 마크업을 추가하면, 구글이 페이지의 내용을 더 정확하게 이해하고, 검색 결과에 리치 스니펫을 표시해줄 수 있다.

메인 페이지에는 WebApplication 스키마를, 가이드 페이지에는 Article 스키마를, FAQ가 포함된 페이지에는 FAQPage 스키마를 적용했다. 카드 백과사전 페이지에는 ItemList 스키마를 사용해서 78장의 카드 목록을 구조화했다.

구조화된 데이터가 검색 순위에 직접적으로 영향을 미치는지는 논쟁의 여지가 있지만, 리치 스니펫으로 표시되면 클릭률이 확실히 올라간다. 같은 검색 결과 페이지에서 별점이나 FAQ가 표시된 결과와 그렇지 않은 결과 중 어느 쪽을 클릭하겠는가.

"처음부터 SEO"가 맞다

돌이켜보면, 프로젝트 시작 시점에 SEO를 고려했어야 했다. 프리렌더링을 나중에 추가하면서 빌드 파이프라인을 크게 수정해야 했고, 메타 태그를 모든 페이지에 추가하는 것도 한꺼번에 하려니 꽤 많은 작업이었다.

처음부터 Next.js를 선택했다면 SSR이 자연스럽게 해결됐을 것이다. 하지만 그때는 Vite의 빠른 개발 경험이 더 중요했고, SEO는 나중에 생각하자고 미뤘다. 사이드 프로젝트에서 흔히 하는 실수다.

교훈은 분명하다. 검색 유입이 필요한 프로젝트라면 SEO는 "나중에"가 아니라 "처음부터"다. 프레임워크 선택, 라우팅 구조, 메타 태그 설계를 초기에 결정해야 한다. 나중에 고치는 비용이 처음에 제대로 하는 비용보다 항상 크다.

다음 편 예고

SEO의 기술적 기반을 닦았지만, 검색 유입을 실제로 만들려면 "검색할 만한 콘텐츠"가 있어야 한다. Part 14에서는 기능 앱에서 콘텐츠 허브로의 전략 전환, 그리고 AI를 활용한 대량 콘텐츠 생산 워크플로우를 다룬다.

댓글

이 블로그의 인기 게시물

사랑을 직접 올리지 않는 설계

감정을 변수로 옮기다 — 3계층 감정 모델

시작의 충동 — "타로 웹앱을 만들어볼까?"