이 블로그를 운영하며 배운 SEO
글은 정상적으로 열리는데 Search Console에는 “리디렉션이 포함된 페이지”가 쌓였습니다. 사이트맵에 적힌 주소와 호스팅이 실제로 보여주는 주소가 달랐습니다. 끝의
/하나를 두고 사이트 설정과 서버가 서로 다른 답을 하고 있었습니다.Docusaurus로 HTML을 미리 만들어 두었으니 검색엔진도 읽기 편할 거라고 생각했습니다. 본문을 읽을 수 있다는 것까지는 맞았습니다. 어느 주소를 대표로 삼고 어떤 페이지를 검색에 보여줄지는 따로 챙겨야 했습니다.
글이 정상적으로 열리는데도 검색 노출을 위해 확인할 일이 남는 이유는 무엇일까요?
이 글에서 다루는 내용
Docusaurus로 만들고 Netlify에 배포하는 이 블로그에서 발견한 문제와 수정 과정을 정리합니다. URL 불일치부터 시작해 사이트맵과 색인 정책, 검색 결과에 쓰이는 제목·설명, 구조화 데이터를 차례로 살펴봅니다.
검색 순위를 올리는 비법이나 유입 증가를 입증한 실험은 아닙니다. 직접 고친 설정과 출력 결과를 바탕으로, 비슷한 규모의 사이트에서 무엇을 확인하면 좋을지 다룹니다. 검색엔진의 동작은 Google 기준입니다.
- 글 발행과 검색 노출 사이
- 같은 글을 가리키는 주소 맞추기
- 검색에 보여줄 페이지 고르기
- 검색 결과와 공유 카드의 제목·설명
- 구조화 데이터의 역할과 자동 생성의 함정
- Search Console에서 확인할 것
- 다시 사이트를 만든다면 확인할 순서
글 발행과 검색 노출 사이
정적 사이트 생성기(Static Site Generator, SSG)는 빌드할 때 본문이 담긴 HTML을 만듭니다. 브라우저에서 JavaScript를 실행하고 API 응답을 기다려야 본문이 생기는 구성보다 내용을 전달하는 과정이 단순합니다.
하지만 HTML을 준비하는 일은 검색에 노출되기까지 거치는 과정의 일부일 뿐입니다.
검색엔진이 글의 주소를 발견해야 방문할 수 있고, 방문한 글도 모두 색인에 등록되는 것은 아닙니다. 색인에 들어갔다고 원하는 검색어에서 눈에 띄는 위치에 나온다는 보장도 없습니다. Google은 이 단계를 검색의 작동 방식에서 구분합니다.
그래서 “검색에 안 나온다”는 문제를 만났을 때 곧바로 메타 태그를 늘리는 것은 순서가 맞지 않을 수 있습니다. 검색봇이 주소를 모르는지, 요청이 실패하는지, 읽었지만 색인하지 않았는지부터 나누어야 합니다. 제 블로그에서는 그중 사이트가 알려준 주소와 실제 응답 주소가 다르다는 문제를 먼저 짚어야 했습니다.
같은 글을 가리키는 주소 맞추기
끝의 슬래시 하나가 만든 불일치
이 블로그에서는 슬래시 없는 주소를 요청하면 Netlify가 슬래시 있는 주소로 보냅니다. 후행 슬래시(trailing slash)는 주소 끝의 /를 말합니다.
GET /docs/intro → 301 리디렉트 → /docs/intro/
GET /docs/intro/ → 200 OK
리디렉트 자체는 정상입니다. 예전 주소나 다른 형태의 주소로 들어온 방문자를 최종 페이지로 안내할 수 있기 때문입니다. 문제는 사이트맵과 표준 URL(canonical)까지 리디렉트되는 쪽을 가리키고 있었다는 점입니다.
canonical은 같은 내용에 여러 주소가 있을 때 대표로 사용할 URL을 제안하는 태그입니다. 당시 상황을 단순화하면 다음과 같습니다.
| 주소를 알려주는 곳 | 가리키던 URL |
|---|---|
| 사이트맵 | /docs/intro |
| 페이지의 canonical | /docs/intro |
| 호스팅이 최종 응답하는 주소 | /docs/intro/ |
/docs/intro와 /docs/intro/는 서로 다른 URL입니다. 어느 형태가 더 좋은지보다 사이트가 대표로 알리는 주소가 최종 응답 주소와 일치하는지가 문제였습니다. canonical도 명령은 아니어서 Google이 다른 URL을 대표로 선택할 수 있습니다. 리디렉트·canonical·사이트맵이 같은 주소를 가리키도록 맞추는 이유입니다. Google의 표준 URL 안내가 이 신호들의 관계를 설명합니다.
설정과 실제 링크를 함께 수정하기
호스팅이 /…/ 형태로 응답하므로 Docusaurus에도 같은 규칙을 지정했습니다.
const config = {
url: "https://blog.wonkooklee.com",
trailingSlash: true,
};
설정만 바꾸고 끝내지는 못했습니다. 직접 작성한 컴포넌트에 <a href="/docs/intro">가 남아 있으면 해당 링크는 여전히 슬래시 없는 주소를 내보냈습니다. 사이트 내부 이동에는 Docusaurus의 Link를 사용하도록 정리했습니다.
import Link from "@docusaurus/Link";
<Link to="/docs/intro">문서 목록</Link>
마크다운 문서끼리 연결할 때는 [문서](./other-page.md)처럼 파일 확장자를 포함했습니다. Docusaurus가 문서 파일을 실제 페이지 주소로 변환하게 하기 위해서입니다. 확장자가 없는 상대 URL은 후행 슬래시 환경에서 다른 경로로 해석될 수 있었습니다.
구조화 데이터도 따로 확인했습니다. 블로그 플러그인이 생성한 URL에는 별도 보정이 필요해서 출력 컴포넌트에서 후행 슬래시를 맞췄습니다. 설정 한 줄이 직접 만든 코드와 모든 플러그인의 출력까지 고쳐주지는 않았습니다.
브라우저가 받은 결과로 확인하기
검증할 때는 설정 파일 대신 최종 응답을 봅니다. 다음 명령은 리디렉트를 따라가지 않고 응답 헤더를 출력하므로 두 주소를 비교하기 좋습니다.
curl -sS -D - -o /dev/null https://blog.wonkooklee.com/docs/intro
curl -sS -D - -o /dev/null https://blog.wonkooklee.com/docs/intro/
첫 응답에서는 상태 코드와 Location을, 두 번째에서는 200을 확인합니다. 이어서 생성된 HTML의 canonical·og:url·구조화 데이터와 사이트맵이 최종 주소를 가리키는지 대조합니다.
이후 Search Console에 슬래시 없는 주소가 “리디렉션이 포함된 페이지”로 남아 있어도 그 사실만으로 수정 실패는 아닙니다. 그 주소는 의도대로 리디렉트되고 있습니다. 확인할 대상은 최종 주소의 색인 상태와 사이트가 여전히 이전 주소를 대표로 알리는지 여부입니다.
Recap
호스팅의 최종 응답 주소에 사이트맵·canonical·내부 링크를 맞췄습니다. 프레임워크 설정과 직접 작성한 출력 코드를 함께 살펴야 했습니다. 리디렉트 URL이 보고서에 남는 것과 최종 페이지에 문제가 있는 것은 구분해서 확인합니다.
검색에 보여줄 페이지 고르기
사이트맵과 색인 제외의 차이
사이트맵을 열어보니 글 외에도 태그, 작성자 목록, 페이지네이션처럼 자동으로 생긴 URL이 많이 들어 있었습니다. 방문자가 글을 찾는 데 쓰는 화면까지 검색 결과에 각각 보여줄 필요가 있는지 돌아보게 됐습니다.
이 블로그에서는 실제 글과 주요 페이지를 중심으로 사이트맵을 정리했습니다. 다만 사이트맵에서 제외하는 것과 검색 결과에서 제외하는 것은 다른 작업입니다.
| 장치 | 전달하는 뜻 | 하지 않는 일 |
|---|---|---|
| 사이트맵 | 발견하고 색인하기를 바라는 대표 URL 목록 제공 | 크롤링·색인 보장, 목록 밖 페이지 차단 |
noindex | 이 페이지를 검색 결과에 넣지 않도록 지시 | 검색봇의 방문 차단 |
robots.txt의 Disallow | 특정 경로를 크롤링하지 않도록 지시 | URL이 검색 결과에 나타나는 것까지 확실히 차단 |
태그·검색·아카이브·페이지네이션처럼 검색 결과에서 제외할 화면에는 noindex, follow를 넣었습니다. 링크를 따라가는 것은 제한하지 않되 해당 화면의 색인을 막는 설정입니다.
<meta name="robots" content="noindex, follow" />
이 페이지들을 robots.txt로도 막으면 검색봇이 HTML 안의 noindex를 읽을 수 없습니다. 색인에서 빼려는 목적이라면 크롤링을 허용해야 한다는 점이 처음에는 조금 낯설었습니다. Google의 noindex 문서에 이 조건이 명시되어 있습니다.
카테고리 페이지는 다르게 두었습니다. 현재 사이트맵에서는 제외하지만 설명문과 글 목록을 제공하므로 noindex를 일괄 적용하지는 않습니다. 자동 생성됐는지만으로 판단하면 독자에게 쓸모 있는 안내 페이지까지 같은 취급을 하게 됩니다.
작은 블로그에서 크롤 예산을 해석하는 법
검색엔진이 한 사이트를 가져가는 데 쓰는 자원에는 한계가 있습니다. 이를 크롤 예산(crawl budget)이라고 부릅니다. 그렇다고 태그 페이지를 줄인 만큼 글을 더 빨리 읽어간다고 계산할 수는 없습니다.
Google의 크롤 예산 가이드는 주로 규모가 크거나 변경이 잦은 사이트, 발견만 되고 크롤링되지 않는 URL이 많은 사이트를 대상으로 합니다. 작은 블로그에서 색인이 늦다는 이유만으로 예산 부족을 원인으로 단정하기는 어렵습니다.
제가 확인한 작업은 사이트맵의 URL을 추리고 페이지별 색인 정책을 명시한 것까지입니다. 그 결과 크롤링이 얼마나 빨라졌는지나 검색 유입이 얼마나 늘었는지는 이 글의 자료로 입증하지 못합니다. 사이트맵 정리를 성과로 설명하려면 전후의 크롤링·색인 기록이 더 필요합니다.
수정일도 비슷합니다. 사이트맵의 lastmod는 정확한 최근 변경일을 전달할 때 의미가 있습니다. Google은 changefreq와 priority를 사용하지 않으며, lastmod도 일관되게 정확한지 확인해 사용한다고 안내합니다. 빌드할 때마다 날짜를 새로 찍는 방식으로 재방문을 예약할 수는 없습니다.
Recap
사이트맵은 URL을 알려주는 목록이고 noindex는 색인 여부를 제어합니다. noindex를 읽게 하려면 크롤링을 허용해야 합니다. 작은 사이트의 색인 지연을 크롤 예산 탓으로 단정하거나 목록 정리만으로 유입이 늘었다고 해석하지 않습니다.
검색 결과와 공유 카드의 제목·설명
글마다 설명이 필요한 이유
초기에는 TechLog 문서에 요약을 넣으면서도 개인 블로그 글에는 제목과 날짜만 적어둔 경우가 있었습니다. 검색과 공유에 사용할 설명을 글마다 준비하지 않은 셈입니다.
지금은 마크다운 앞부분의 프론트매터(frontmatter)에 description을 필수로 두고 누락을 검사합니다. 검색 결과에 들어가기 전에 어떤 글인지 판단할 수 있도록 내용을 한두 문장으로 적습니다.
# 프론트매터 일부
title: "이 블로그를 운영하며 배운 SEO"
description: "Docusaurus 블로그에서 URL 불일치와 색인 정책을 정리한 과정입니다. 메타데이터·구조화 데이터의 역할과 Search Console 점검 방법을 다룹니다."
이 블로그는 설명을 150자 미만으로 제한하지만 이는 작성·검수 규칙입니다. Google이 정한 고정 글자 수는 아닙니다. 실제 검색 스니펫은 화면 폭과 검색어에 따라 달라집니다.
또한 description을 적어도 검색 결과에 그대로 나오는 것은 아닙니다. Google은 본문에서 스니펫을 만들고, 메타 설명이 더 적합하다고 판단하면 이를 사용합니다. 같은 글도 검색어에 따라 다른 부분이 표시될 수 있습니다. 검색 스니펫 안내를 보면 작성자가 제공하는 설명과 최종 표시 문구의 차이를 확인할 수 있습니다.
제목도 글의 내용을 짐작할 수 있어야 합니다. 이 블로그에는 대화체 제목을 본문에 두고 title_meta로 검색용 제목을 따로 지정하는 글이 있습니다. 이 경우에도 본문이 다루지 않는 검색어를 끼워 넣지 않고 실제 질문과 답을 요약합니다.
공유 카드가 맡는 일
메신저나 SNS에 링크를 붙일 때는 Open Graph 메타데이터가 카드의 재료가 됩니다. og:title, og:description, og:image, og:url로 제목·설명·대표 이미지·주소를 전달합니다. Open Graph 규격은 이 속성들을 정의합니다.
공유 카드는 검색 순위와 별개로, 링크를 받은 사람이 글을 열지 판단하는 근거가 됩니다. 저는 대표 이미지가 있는 글에는 해당 이미지를 쓰고 없는 글에는 공통 소셜 카드가 나오도록 했습니다. 플랫폼이 예전 카드를 캐시할 수 있으므로 HTML 수정과 실제 카드 갱신도 구분해서 확인합니다.
새 글마다 요약 누락을 떠올리는 대신 검사 스크립트가 잡도록 바꾼 것은 계속 도움이 되는 선택이었습니다. 단, 검사기는 설명의 존재와 길이까지 확인할 뿐입니다. 요약을 읽고 글의 내용을 예상할 수 있는지는 직접 읽어봐야 합니다.
구조화 데이터의 역할과 자동 생성의 함정
구조화 데이터는 페이지의 제목·저자·글 유형 같은 정보를 기계가 읽을 수 있는 형식으로 적는 방법입니다. 이 블로그는 schema.org 어휘를 JSON-LD 형식으로 전달합니다.
사이트와 저자는 WebSite·Person, 기술 문서는 Article, 블로그 포스트는 BlogPosting으로 표현합니다. 같은 저자를 여러 번 따로 정의하지 않도록 @id로 연결합니다. 다음은 기술 문서에 들어가는 정보 중 일부를 단순화한 예시입니다.
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "이 블로그를 운영하며 배운 SEO",
"author": { "@id": "https://blog.wonkooklee.com/#person" },
"isPartOf": { "@id": "https://blog.wonkooklee.com/#website" }
}
여기서 실수했던 부분은 Docusaurus가 이미 생성하는 데이터를 확인하지 않고 추가한 것입니다. 블로그 플러그인은 BlogPosting을 자동으로 만들고 있었습니다. 같은 글을 설명하는 블록을 하나 더 넣으면 제목이나 URL을 수정할 때 양쪽이 어긋날 수 있습니다.
그래서 블로그는 기존 생성 결과를 받아 필요한 필드를 보정하고, 문서에는 별도로 Article을 넣었습니다. 목차 역할의 인덱스 페이지는 글이 아니므로 CollectionPage로 구분했습니다. JSON-LD 블록의 개수 자체보다 같은 대상을 중복해서 관리하거나 실제 화면과 다른 정보를 쓰고 있지 않은지를 확인했습니다.
구조화 데이터는 Google이 내용을 이해하고 지원되는 검색 표현을 만드는 데 도움을 줄 수 있습니다. 그러나 올바른 마크업을 넣어도 리치 결과 노출은 보장되지 않습니다. Google의 구조화 데이터 안내와 Rich Results Test를 함께 참고하되, 테스트 통과를 검색 성과로 읽지는 않습니다. Rich Results Test는 Google이 지원하는 리치 결과를 대상으로 하므로 모든 schema.org 유형을 검증하는 도구도 아닙니다.
Search Console에서 확인할 것
수정한 HTML이 의도대로 나오는지는 로컬 빌드로 확인할 수 있습니다. Google이 어떤 URL을 발견했고 색인했는지는 Search Console에서 확인합니다.
처음에는 소유권을 확인하고 사이트맵을 제출합니다. 그다음 페이지 색인 보고서에서 실제 글의 URL을 골라 봅니다. 이 블로그에서는 “색인되지 않음”이라는 묶음 안에 정상 리디렉트와 더 살펴볼 글이 함께 있어 상태를 나누어 읽어야 했습니다.
| 보고서의 상태 | 확인할 내용 |
|---|---|
| 발견됨: 현재 색인이 생성되지 않음 | Google이 URL을 알지만 아직 크롤링하지 않은 상태. 내부 링크·서버 상태와 오래 지속되는지 확인 |
| 크롤링됨: 현재 색인이 생성되지 않음 | 읽었지만 색인하지 않은 상태. 본문 누락·중복·콘텐츠의 유용성 등을 점검하되 상태명만으로 원인을 단정하지 않음 |
| 리디렉션이 포함된 페이지 | 최종 도착 URL이 의도한 주소인지, 그 주소가 색인 대상인지 확인 |
| 적절한 표준 태그가 포함된 대체 페이지 | 의도한 중복 처리라면 정상. 대표로 선택된 URL 확인 |
noindex 태그에 의해 제외됨 | 태그·검색 화면이면 의도한 결과일 수 있음. 실제 글이라면 설정 확인 |
상태의 정의는 페이지 색인 보고서 도움말을 기준으로 읽습니다. 미색인 URL의 총수를 0으로 만드는 것을 목표로 잡으면 일부러 제외한 페이지까지 다시 색인하려고 손댈 수 있습니다.
특정 글은 URL 검사에서 더 자세히 봅니다. Google이 마지막으로 확인한 상태와 현재 페이지를 검사하는 실시간 테스트는 기준 시점이 다릅니다. 실시간 테스트가 통과해도 이미 색인됐다는 뜻은 아닙니다. 대표 URL 정보나 참조 페이지가 제공되면 사이트가 의도한 주소와 비교하고, 필요하면 수정 후 색인 생성을 요청합니다. 요청도 색인을 보장하지는 않습니다. URL 검사 안내에 두 검사 결과의 차이가 정리되어 있습니다.
색인 상태를 확인한 뒤에는 실적 보고서에서 페이지별 검색어·노출·클릭을 봅니다. 노출이 적은 글과 노출은 있지만 클릭이 적은 글은 살펴볼 질문이 다릅니다. 후자라면 검색어와 글의 내용이 맞는지, 제목과 설명이 그 내용을 전달하는지 확인할 수 있습니다. 다만 순위·기기·검색 결과 구성도 클릭에 영향을 주므로 클릭률 변화만으로 문구의 효과를 단정하지 않습니다.
다시 사이트를 만든다면 확인할 순서
같은 블로그를 다시 만든다면 다음 순서로 확인하겠습니다.
| 순서 | 질문 | 확인할 곳 |
|---|---|---|
| 본문 접근 | 로그인 없이 글의 HTML을 가져갈 수 있는가? | 최종 HTTP 응답과 HTML 본문 |
| 주소 일치 | 사이트가 알려주는 주소가 곧바로 글을 반환하는가? | 리디렉트·canonical·사이트맵·내부 링크 |
| 발견과 색인 정책 | 글로 가는 링크가 있고, 검색에 보여줄 페이지가 구분돼 있는가? | 문서 목록·관련 글·사이트맵·noindex |
| 내용 설명 | 제목·설명·대표 이미지가 글의 내용을 전달하는가? | 프론트매터와 생성된 메타 태그 |
| 구조화 데이터 | 페이지 유형에 맞고 기존 출력과 충돌하지 않는가? | 최종 JSON-LD와 검증 도구 |
| 검색 반영 | 실제 글이 색인되고 어떤 검색어에 노출되는가? | Search Console 색인·실적 보고서 |
처음에는 화면에 글이 잘 보이면 발행이 끝났다고 생각했습니다. 이제는 새 글을 올리고 나면 그 글로 향하는 링크와 생성된 메타데이터도 함께 봅니다. Search Console에서 리디렉트 주소를 다시 만나더라도 우선 최종 주소부터 확인합니다. 슬래시 하나 때문에 헤맨 뒤에 생긴 작은 습관입니다.
References
발견·색인·URL
- Google Search Central: Google 검색의 작동 방식
- Google Search Central: 표준 URL 지정
- Google Search Central: 사이트맵 작성과 제출
- Google Search Central:
noindex로 색인 차단 - Google Crawling Infrastructure: 크롤 예산
검색 표현과 측정
- Google Search Central: 검색 스니펫
- Google Search Central: 구조화 데이터 소개
- Google Search Console: 페이지 색인 보고서
- Google Search Console: URL 검사 도구
- Open Graph protocol
Docusaurus 구현
