← 글

Google이 내 글을 찾지 못하게 두지 마세요——SSR Blog의 SEO 시행착오 기록

· 9 분 읽기 · 4 회 조회
Astro SEO SSR Sitemap JSON-LD
SSR Blog 실전

Google이 내 글을 찾지 못하게 두지 마세요

SSR Blog의 SEO 시행착오 기록

Astro + PostgreSQL로 Blog를 만들면서 내린 모든 SEO 결정의 이유와 글이 검색 결과에서 사라질 뻔했던 함정들을 정리했습니다.

문제

SSR Blog의 SEO는 왜 정적 사이트와 다를까요?

1

동적 콘텐츠가 Sitemap에 없음

SSR 페이지는 runtime에 생성되지만,@astrojs/sitemap 에는 build-time에 생성된 정적 페이지만 포함됩니다.

글을 발행했는데 sitemap에 없다면 Google은 그 글이 존재하는지조차 모릅니다.

2

Tool Schema만이 유일한 숨은 비용은 아닙니다

MCP token처럼 SEO에도 ‘숨은 비용’이 있습니다. 누락된 meta tag, 잘못된 hreflang, 일치하지 않는 canonical입니다.

67% 다국어 사이트의 hreflang 구현에 오류가 있음

3

SSR 성능의 양날의 검

SSR은 콘텐츠를 실시간으로 업데이트할 수 있게 하지만, TTFB가 DB 쿼리 시간의 영향을 받게도 합니다.

캐시 전략이 없으면 = request마다 DB query를 실행합니다.

기술 스택

SSR Blog의 구성

Astro 5
프레임워크
PostgreSQL
데이터베이스
Drizzle
ORM 쿼리 계층
Tailwind
스타일
Cloudflare
Tunnel 배포
SEO 결정

우리는 무엇을 했을까요?

동적 sitemap부터 robots.txt까지 다섯 계층을 보완했고, 각 계층에는 명확한 이용자가 있습니다.

동적 Sitemap
sitemap-posts.xml DB에서 발행된 글을 실시간으로 조회해 build할 때 자동으로 패치해 넣습니다: sitemap-index.xml
🔍 Google / Bing 크롤러
JSON-LD 구조화 데이터
글마다 BlogPosting schema를 자동으로 생성합니다——제목, 작성자, 발행 시각, 언어를 모두 DB 필드에 연결합니다.
📊 검색 결과의 리치 스니펫
Canonical URL
SSR 페이지의 canonical이 자동으로 자기 자신을 가리키게 설정합니다. http://localhost 와 https://blog.joneshong.com 가 중복으로 판정되지 않도록 합니다.
🔗 중복 제거 메커니즘
hreflang 다국어 설정
zh-TW ↔ en 양방향 alternate link에 x-default fallback을 추가합니다. 사이트의 67%가 이 단계를 잘못 구현합니다.
🌐 다국어 검색
robots.txt
크롤러가 모든 공개 페이지에 접근하도록 허용하고 /admin/ 와 /api/는 차단합니다. 두 개의 sitemap을 가리킵니다.
🤖 크롤러 제어
성능

Core Web Vitals 최적화

0 KB
JS baseline
Astro는 기본적으로 JS를 보내지 않습니다
필요할 때만
React island hydration
검색, 그래프
지연
글꼴 로딩 전략
CJK 시스템 글꼴 + Latin lazy load
SSR
동적 콘텐츠
실시간 업데이트

Astro의 비장의 무기는 다음과 같습니다.기본적으로 JavaScript를 전혀 보내지 않습니다. React 컴포넌트는 상호작용이 필요한 곳에서만 hydrate하고(client:only), 나머지는 모두 정적 HTML입니다.

시행착오 기록

우리는 어떻게 문제를 발견했을까요?

처음부터 설계한 것이 아니라, sitemap을 아무도 참조하지 않고 읽기 시간이 73분으로 계산되며 Nginx subpath 페이지가 비는 세 가지 문제를 겪은 뒤에야 보완했습니다.

글 발행 → Google에서 검색되지 않음

sitemap-posts.xml 아무도 참조하지 않았습니다

@astrojs/sitemap 생성할 sitemap-index.xml 때 정적 페이지만 포함합니다. 동적 sitemap-posts.xml 는 무시됐습니다. 해결책은 build 후 sitemap-index를 자동으로 패치해 posts sitemap을 추가하는 것이었습니다.

깨달은 점:자동화 도구가 모든 case를 다루는 것은 아닙니다. SSR 동적 페이지의 누락은 직접 메워야 합니다.

12분짜리 글이 73 min read로 표시됨

Rich HTML의 content.length 에 전체 CSS가 포함됨

content.length / 400 로 읽기 시간을 계산했지만, Rich HTML 형식의 content에는 25000+ 글자의 CSS + HTML tags가 있습니다. 먼저 tags를 strip해야 읽기 시간이 정확하게 계산됩니다.

깨달은 점:content 길이와 관련된 로직이라면, 먼저 content의 형식이 무엇인지 확인해야 합니다.

Nginx subpath proxy 아래에서 페이지가 비어 있음

Vite dev server + subpath = 막다른 길

Vite의 ES module import는 절대 경로를 사용하므로 nginx의 sub_filter 를 계속 고쳐야 합니다. 해결책은 dev preview에 Vite dev server 대신 빌드된 SSR server를 쓰는 것입니다.

깨달은 점:Astro islands의 component-url 속성도 sub_filter로 다시 써야 합니다.

도구

내 페이지의 head를 붙여 넣어 확인하기

위 목록의 여섯 항목 중 세 항목은 사이트를 크롤링하지 않고도 검증할 수 있습니다. 내 글 중 하나의 <head> 소스 코드만 붙여 넣으면 됩니다.canonical, hreflang, JSON-LD 세 항목은 모두 브라우저 안에서 로컬로 분석하며 아무것도 전송하지 않습니다. 나머지 세 항목(sitemap 수록 여부, robots.txt, TTFB)은 실제로 내 도메인에 요청을 보내야 하는데, 브라우저의 교차 출처 제한 때문에 여기서는 할 수 없습니다. 이 세 항목은 위의 prompt를 AI에 전달해 크롤링하도록 하세요.

검사 대상은 ‘붙여 넣은 문자열’이지, 온라인 페이지가 아닙니다. 브라우저 DevTools의 Elements 패널(JS 실행 후의 DOM)을 붙여 넣으면 크롤러가 보는 내용과 결과가 다를 수 있습니다. 크롤러가 보는 내용과 맞추려면 ‘페이지 소스 보기’(view-source)의 내용을 사용하세요.

이 부분을 가져가세요

AI Agent에게 복사해 전달하세요

글을 발행했다고 생각했는데, Google은 그 글의 존재조차 모를 수 있습니다——SSR blog 페이지는 runtime에야 생성되고 @astrojs/sitemap은 build 시의 정적 페이지만 수집하기 때문에 동적 글은 sitemap에 아예 기록되지 않습니다. meta tag가 빠지면 기껏해야 순위가 낮아지지만, 색인되지 않으면 검색 결과에서 완전히 사라집니다.

아래 내용을 AI에 붙여 넣으세요. 일반적인 상식으로 답하지 말고 내 사이트를 직접 크롤링한 뒤 이 글의 목록과 대조해 실제로 무엇이 빠졌는지 찾아내도록 하세요:

저는 SSR blog의 SEO 시행착오 기록을 읽고 있습니다 (https://blog.joneshong.com/zh/blog/hello-world). 작성자는 Astro + PostgreSQL로 사이트를 만들면서 SEO 결정 5가지와 SSR 특유의 함정 3가지를 정리했습니다. 제 사이트도 같은 문제를 겪고 있는지 확인해 주세요. 일반적인 SEO 상식에 기대어 답하지 말고 실제 출력 결과를 직접 검증해 주세요. 제 사이트: [URL 입력], SSR / SSG / 순수 정적 사이트 중 하나입니다(확실하지 않다면 먼저 판별해 주세요). 다음 항목을 순서대로 실제 사이트에서 확인해 주세요(이 글에서 겪은 문제의 순서대로). 1. sitemap을 가져와 ‘최근 발행한 동적 페이지 몇 개’가 실제로 들어 있는지 확인해 주세요—— SSR에서 가장 흔한 문제는 동적 페이지가 sitemap에 수록되지 않아 Google이 그 존재를 모르는 것입니다. 2. 글 하나의 head 태그를 확인해 주세요. canonical이 자기 자신을 가리키나요? localhost나 미리보기 도메인이 남아 있나요? 3. hreflang에 양방향 alternate + x-default가 있나요? (이 글에서 확인한 다국어 사이트의 67%가 이 단계에서 잘못됐습니다.) 4. robots.txt를 가져와 확인해 주세요. /admin/, /api/ 같은 비공개 경로를 차단하나요? sitemap을 가리키나요? 5. 모든 글에 JSON-LD BlogPosting이 있나요? 필드는 실제 데이터에서 생성하나요, 아니면 하드코딩했나요? 6. 제 사이트가 SSR이라면 DB query 때문에 TTFB가 느려지나요? 캐시 계층이 있나요? 답변에는 제가 실제로 빠뜨리거나 잘못 구현한 항목과 확인한 증거를 적고, ‘글이 검색 결과에서 사라질 수 있는지’를 기준으로 우선순위를 매겨 주세요. 순수 정적 사이트라면 1번과 6번은 해당하지 않는다고 바로 말하고 억지로 적용하지 마세요.
참고 자료

더 읽어보기

이 다섯 글은 각각 한 가지 결정에 대응하며, 숨은 비용에 대한 비유부터 hreflang 오류율 67%라는 데이터까지 다룹니다.

글 왜 중요한가
Apideck: Your MCP Server Is Eating Your Context Window ‘숨은 비용’이라는 비유에 영감을 주었습니다
SEO for Astro: How to Make the Fastest Framework Also the Smartest Astro SEO 구현에 관한 종합 가이드
JSON-LD Blog Post Example BlogPosting schema의 참고 구현
Multilingual SEO and Hreflang Guide 67% 오류율 데이터의 출처
How to Improve Core Web Vitals SSR 성능을 최적화하는 구체적인 방법
결론

SEO는 공학이지 미신이 아닙니다

모든 결정에는 이유가 있고, 모든 함정에는 교훈이 있습니다.
SSR Blog의 SEO는 meta tag를 설정하면 끝나는 일이 아니라 계속 개선해 나가는 엔지니어링 작업입니다.

// 의사결정 과정

문제 1: 동적 글이 sitemap에 없음
→ 직접 추가 sitemap-posts.xml

문제 2: SSR에 캐시가 없음
→ 정적 페이지 prerender + 동적 페이지 SSR

문제 3: 다국어 SEO는 오류가 나기 쉬움
  → hreflang 양방향 + x-default

검증: Google Search Console에 직접 제출 + 모니터링

// 요약
Astro SSR + 자동화 SEO pipeline
수록 누락 없이, 중복 없이, 지연 없이
✦ 프롬프트 복사