XML 사이트맵 검사기

모든 프로토콜 규칙 확인. URL 제한도 가입도 없습니다.

원본 서버에서 직접 가져옵니다. 많은 서버가 이를 차단하므로, 차단된다면 내용을 붙여넣으세요.
입력
대기 중문서를 붙여넣으면 검사합니다. 입력하는 동안 검증이 실행됩니다.

모든 처리는 이 탭 안에서 이루어집니다. 붙여넣은 내용은 업로드되거나 기록되거나 전송되지 않습니다. 네트워크 패널을 열어 확인하세요.

위에 사이트맵을 붙여넣거나 주소를 입력하고 가져오기를 누르면, sitemaps.org 프로토콜의 모든 규칙을 한 번에 검사합니다. URL 개수가 50,000개 한도를 넘지 않는지, 압축하지 않은 크기가 50 MB 한도를 넘지 않는지, 그리고 loc, lastmod, changefreq, priority 각각의 형식까지. 사이트맵 인덱스 파일에는 별도의 규칙 묶음이 적용됩니다.

대부분은 Search Console에서 "사이트맵을 읽을 수 없음"이나 "사이트맵을 읽을 수 있지만 오류가 있음"을 들고 옵니다. 둘 다 줄 번호를 알려 주지 않습니다. 여기서는 지적마다 문제 삼은 값을 인용하고, 근거 규칙을 밝히고, 대신 무엇을 써야 하는지 알려 줍니다.

이 검색어의 다른 도구와 두 가지가 다릅니다. 아무것도 업로드하지 않으므로, 아직 공개하지 않은 페이지가 들어 있는 스테이징 사이트맵도 여러분의 컴퓨터에 머무릅니다. 그리고 실행하지 못한 검사는 "통과"가 아니라 "실행하지 않음"으로 보고합니다.

두 개의 확고한 한도

사이트맵 하나에는 URL 50,000개를 담을 수 있고, 압축하지 않은 크기는 50 MB까지입니다. 프로토콜은 이를 52,428,800바이트로 명시합니다. 둘 다 검사하며, 바이트 크기는 한도에 가깝든 아니든 항상 보고합니다. 파일을 들여다봐서는 가늠할 수 없는 유일한 숫자이기 때문입니다.

gzip은 여유를 만들어 주지 않습니다. 한도는 압축을 푼 뒤에 적용되므로, 3 MB짜리 sitemap.xml.gz가 61 MB로 펼쳐지면 한도 초과입니다. 사이트맵 인덱스에도 같은 두 한도가 적용됩니다. 둘 다 90퍼센트에서 경고합니다. 늘어나는 카탈로그로 만든 사이트맵은 평범한 화요일에 선을 넘기 때문입니다.

  • 사이트맵 파일 하나당 <url> 항목 50,000개.
  • gzip 여부와 무관하게, 압축하지 않은 상태로 52,428,800바이트.
  • <loc> 값 하나당 2,048자.
  • 사이트맵 인덱스 하나당 <sitemap> 항목 50,000개.

네임스페이스는 정확해야 합니다

루트는 <urlset>이고, 인덱스라면 <sitemapindex>이며, http://www.sitemaps.org/schemas/sitemap/0.9를 선언해야 합니다. 사람들이 걸리는 지점이 셋 있습니다. https 사이트라도 https가 아니라 http라는 점, 끝에 슬래시가 없다는 점, 그리고 버전이 0.9이며 처음부터 계속 그랬다는 점입니다.

틀려도 크게 항의하는 것이 없습니다. 파일은 여전히 적격 형식이고 브라우저에서도 열립니다. 검색 엔진은 그저 잘못된 네임스페이스의 요소를 인식하지 않을 뿐이며, 그 결과 여러분의 사이트맵을 "아무것도 들어 있지 않은 문서"로 읽습니다.

<url>과 <sitemap> 자식을 섞는 것은 오류입니다. 파일은 사이트맵이거나 인덱스이거나 둘 중 하나이기 때문입니다. 선언하는 인코딩은 UTF-8이어야 합니다. 확장 요소는 선언된 접두사가 있는지만 확인하며, 이미지·동영상·뉴스 규칙 자체는 여기서 검증하지 않습니다.

lastmod, changefreq, priority

lastmod는 W3C Datetime이어야 합니다. YYYY-MM-DD만으로도 유효합니다. 전체 타임스탬프는 2026-03-14T09:30:00+00:00처럼 시간대가 붙어 있으면 유효합니다. T 대신 공백, 유닉스 타임스탬프, 14/03/2026, 시간대 없는 타임스탬프는 모두 오류입니다.

다른 무료 검사기는 하지 않는 두 번째 lastmod 검사가 돌아갑니다. 항목 다섯 개 이상이 lastmod를 갖고 있고 값이 전부 똑같다면 경고가 나옵니다. 여러분의 생성기가 콘텐츠가 바뀐 날짜가 아니라 빌드 시각을 찍고 있다는 뜻입니다. 구글은 lastmod가 일관되고 검증 가능할 만큼 정확할 때만 그것을 사용하므로, 그런 패턴은 이 필드를 무시하라고 구글에게 가르치는 셈입니다.

changefreq가 받는 값은 소문자 일곱 개뿐입니다. always, hourly, daily, weekly, monthly, yearly, never. priority는 0.0에서 1.0 사이여야 합니다. 둘 다 존재하기만 해도 정보로 보고합니다. 구글이 자사 문서에서 둘 다 무시한다고 밝히고 있기 때문입니다.

같은 호스트, 같은 디렉터리, 그리고 URL로 가져오기

https://example.com/catalog/sitemap.xml 에 있는 사이트맵은 https://example.com/catalog/ 아래의 URL을 담을 수 있고, https://example.com/images/ 아래의 URL은 담을 수 없습니다. 같은 호스트, 같은 스킴, 자기 디렉터리이거나 그 아래. 이 규칙들에는 사이트맵이 게시될 주소가 필요한데, 그 주소는 파일 안에 들어 있지 않습니다.

URL 입력란이 바로 그 용도이며, 가져오지 않고 주소만 입력해도 됩니다. 입력하면 세 가지 검사가 켜집니다. 다른 호스트는 오류, 다른 스킴은 오류, 사이트맵 자신의 디렉터리 밖에 있는 loc는 경고입니다. 비워 두면 셋 다 조용히 통과되는 대신 "실행하지 않음"으로 표시됩니다.

가져오기는 프록시를 거치지 않고 브라우저에서 여러분의 서버로 바로 파일을 요청합니다. 그래서 많은 호스팅에서 CORS에 막힙니다. 막히면 그 이유와, 직접 실행할 수 있는 curl 명령을 알려 줍니다.

코드로 사이트맵 검사하기

빌드 단계에 넣어 둘 가치가 있습니다. 50,000 URL 한도를 넘긴 사이트맵이 색인이 아니라 배포를 실패시키게 되니까요. 각 예제는 URL을 세고, 압축하지 않은 크기를 재고, lastmod를 검사합니다.

// Node 18 or later.  npm i @xmldom/xmldom
import { DOMParser } from '@xmldom/xmldom';

const SITEMAP_NS = 'http://www.sitemaps.org/schemas/sitemap/0.9';
const MAX_URLS = 50_000;
const MAX_BYTES = 52_428_800;

const response = await fetch('https://example.com/sitemap.xml');
const xml = await response.text();
const bytes = Buffer.byteLength(xml, 'utf8');

// xmldom does not resolve external entities, so there is no XXE surface here.
const doc = new DOMParser().parseFromString(xml, 'text/xml');
const root = doc.documentElement;

if (root.namespaceURI !== SITEMAP_NS) {
  throw new Error('Namespace is "' + root.namespaceURI + '", expected "' + SITEMAP_NS + '"');
}

const locs = doc.getElementsByTagNameNS(SITEMAP_NS, 'loc');
console.log(locs.length + ' URLs, ' + bytes + ' bytes uncompressed');
if (locs.length > MAX_URLS) throw new Error('Over the 50,000 URL limit');
if (bytes > MAX_BYTES) throw new Error('Over the 50 MB limit');

const lastmods = doc.getElementsByTagNameNS(SITEMAP_NS, 'lastmod');
for (let i = 0; i < lastmods.length; i++) {
  const v = lastmods.item(i).textContent.trim();
  // W3C Datetime: a bare date, or a timestamp that carries a zone.
  const ok = /^\d{4}-\d{2}-\d{2}$/.test(v)
    || (/^\d{4}-\d{2}-\d{2}T/.test(v) && /(Z|[+-]\d{2}:\d{2})$/.test(v));
  if (!ok) console.error('Invalid lastmod: ' + v);
}
# pip install lxml requests
import re
import sys
import requests
from lxml import etree

SITEMAP_NS = 'http://www.sitemaps.org/schemas/sitemap/0.9'
MAX_URLS, MAX_BYTES = 50_000, 52_428_800

raw = requests.get('https://example.com/sitemap.xml', timeout=30).content

# The three flags are the safe form. lxml will otherwise expand entities and
# fetch a DTD the document references.
parser = etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False)
root = etree.fromstring(raw, parser)

if etree.QName(root).namespace != SITEMAP_NS:
    sys.exit('Wrong namespace: %s' % etree.QName(root).namespace)

locs = root.findall('{%s}url/{%s}loc' % (SITEMAP_NS, SITEMAP_NS))
print('%d URLs, %d bytes uncompressed' % (len(locs), len(raw)))
if len(locs) > MAX_URLS:
    sys.exit('Over the 50,000 URL limit')
if len(raw) > MAX_BYTES:
    sys.exit('Over the 50 MB limit')

W3C = re.compile(r'^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2}))?$')
for lm in root.iter('{%s}lastmod' % SITEMAP_NS):
    if not W3C.match((lm.text or '').strip()):
        print('Invalid lastmod:', lm.text)

# The protocol publishes an XSD, which lxml can check structure against:
#   schema = etree.XMLSchema(etree.parse('sitemap.xsd'))
#   schema.assertValid(root.getroottree())
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.format.DateTimeParseException;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;

final String NS = "http://www.sitemaps.org/schemas/sitemap/0.9";

byte[] bytes = HttpClient.newHttpClient()
    .send(HttpRequest.newBuilder(URI.create("https://example.com/sitemap.xml")).build(),
          HttpResponse.BodyHandlers.ofByteArray())
    .body();

DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
// Without setNamespaceAware(true) every namespace lookup below returns nothing.
// This is the single most common reason sitemap parsing code silently finds zero URLs.
f.setNamespaceAware(true);
f.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
f.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
f.setXIncludeAware(false);

Document doc = f.newDocumentBuilder().parse(new ByteArrayInputStream(bytes));
NodeList locs = doc.getElementsByTagNameNS(NS, "loc");

System.out.printf("%d URLs, %d bytes uncompressed%n", locs.getLength(), bytes.length);
if (locs.getLength() > 50_000) System.err.println("Over the 50,000 URL limit");
if (bytes.length > 52_428_800) System.err.println("Over the 50 MB limit");

NodeList lastmods = doc.getElementsByTagNameNS(NS, "lastmod");
for (int i = 0; i < lastmods.getLength(); i++) {
    String v = lastmods.item(i).getTextContent().trim();
    try {
        if (v.length() == 10) LocalDate.parse(v);
        else OffsetDateTime.parse(v);   // rejects a timestamp with no zone
    } catch (DateTimeParseException e) {
        System.err.println("Invalid lastmod: " + v);
    }
}
using System.Globalization;
using System.Xml;

const string Ns = "http://www.sitemaps.org/schemas/sitemap/0.9";

// XmlReader streams, so a 50 MB sitemap never becomes a 400 MB DOM.
var settings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Prohibit,  // external entities are never fetched
    XmlResolver = null,
    IgnoreWhitespace = true,
};

string[] w3c =
{
    "yyyy-MM-dd",
    "yyyy-MM-ddTHH:mmK",
    "yyyy-MM-ddTHH:mm:ssK",
    "yyyy-MM-ddTHH:mm:ss.FFFFFFFK",
};

byte[] bytes = await new HttpClient().GetByteArrayAsync("https://example.com/sitemap.xml");

int urls = 0;
using var stream = new MemoryStream(bytes);
using var reader = XmlReader.Create(stream, settings);
while (reader.Read())
{
    if (reader.NodeType != XmlNodeType.Element || reader.NamespaceURI != Ns) continue;

    if (reader.LocalName == "loc")
    {
        urls++;
    }
    else if (reader.LocalName == "lastmod")
    {
        var v = reader.ReadElementContentAsString().Trim();
        // TryParse would accept 03/14/2026. TryParseExact against the four
        // permitted layouts is what the protocol actually asks for.
        if (!DateTimeOffset.TryParseExact(v, w3c, CultureInfo.InvariantCulture,
                                          DateTimeStyles.None, out _))
        {
            Console.Error.WriteLine("Invalid lastmod: " + v);
        }
    }
}

Console.WriteLine(urls + " URLs, " + bytes.Length + " bytes uncompressed");
if (urls > 50_000) Console.Error.WriteLine("Over the 50,000 URL limit");
if (bytes.Length > 52_428_800) Console.Error.WriteLine("Over the 50 MB limit");
# The protocol publishes an XSD, so xmllint can check the structure offline.
curl -sL https://example.com/sitemap.xml -o sitemap.xml
curl -sO https://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd

xmllint --noout --nonet --schema sitemap.xsd sitemap.xml

# The two hard limits:
xmllint --nonet --xpath 'count(//*[local-name()="loc"])' sitemap.xml   # cap 50,000
wc -c < sitemap.xml                                                    # cap 52,428,800

# A gzipped sitemap must be decompressed before either check:
curl -sL https://example.com/sitemap.xml.gz | gunzip > sitemap.xml

# Pull every URL out, one per line, for a crawler or a spreadsheet:
xmllint --nonet --xpath '//*[local-name()="loc"]/text()' sitemap.xml | tr -s '\n'

보안 플래그는 장식이 아닙니다. Java도 Python도 C#도, 달리 지시하지 않으면 외부 엔티티를 해석합니다. 그리고 사이트맵은 보통 여러분이 쓴 것이 아니라 플러그인에서 네트워크를 타고 온 파일입니다. xmllint의 --nonet 플래그도 같은 일을 합니다.

자주 묻는 질문

Search Console은 사이트맵에 오류가 있다는데 이 도구는 괜찮다고 합니다. 왜죠?

둘은 서로 다른 것을 봅니다. 이 페이지는 파일을 검사합니다. 구조, 네임스페이스, 한도, 날짜, 그리고 사이트맵 주소를 알려 주면 호스트와 디렉터리 규칙까지. Search Console은 거기에 더해 크롤러만 알 수 있는 것을 확인합니다. 먼저 그 파일에 도달할 수 있었는지부터요.

"가져올 수 없음"은 대개 404, 리디렉션 연쇄, 사이트맵 경로를 덮는 robots.txt 규칙, 또는 200 상태로 전달된 HTML 오류 페이지를 뜻합니다. 여기서 URL을 가져오면 진짜 상태 코드가 보입니다. 또 하나 흔한 불일치는 URL은 유효한데 그 뒤의 페이지가 noindex이거나 사라진 경우이며, 이는 크롤링 문제입니다.

구글이 changefreq와 priority를 사용하나요?

아닙니다. 구글은 사이트맵 문서에서 둘 다 무시한다고 밝히고 있습니다. 이 도구는 그것들의 존재를 오류가 아니라 정보로 보고합니다. 합법적인 요소이고 해가 되지도 않기 때문입니다.

실질적으로는, priority 값을 어떻게 배치해도 어떤 페이지가 다른 페이지보다 먼저 크롤링되지 않고, changefreq를 hourly로 둬도 크롤링 빈도는 올라가지 않습니다. 그것들을 계산하는 코드를 유지보수하고 있다면, 그 코드는 제 몫을 하지 못하고 있는 것입니다. 예외는 lastmod이며, 제대로 쓸 가치가 있는 유일한 선택 요소입니다.

lastmod는 어떤 형식이어야 하나요?

W3C Datetime, ISO 8601의 부분집합입니다. 가장 단순한 유효 형식은 날짜만 쓴 2026-03-14입니다. 시각을 넣는다면 시간대도 넣어야 합니다. 2026-03-14T09:30:00Z와 2026-03-14T09:30:00+00:00 둘 다 올바릅니다.

거부되는 형식은 하필 데이터베이스와 템플릿이 자연스럽게 만들어 내는 것들입니다. T 대신 공백은 무효이고, 시간대 없는 타임스탬프도 무효이며, 유닉스 에포크 숫자나 14/03/2026처럼 일자를 앞세운 표기도 마찬가지입니다. 잘못된 날짜가 여러분의 코드가 아니라 플러그인에서 온다면, 오류에 인용된 값으로 어떤 플러그인인지 알 수 있는 경우가 많습니다.

사이트맵 하나에 URL을 몇 개까지 넣을 수 있나요?

50,000개이며, 동시에 압축하지 않은 상태로 52,428,800바이트 미만이어야 합니다. 둘 중 먼저 닿는 쪽이 중요하고, URL이 긴 사이트에서는 보통 개수보다 크기 한도가 먼저 옵니다.

어느 쪽이든 넘으면 파일을 나누고, 나눈 것들을 사이트맵 인덱스에 나열하세요. 같은 네임스페이스의 작은 파일이고, 자식은 <sitemap> 요소이며 각각에 <loc>이 하나씩 들어갑니다. 인덱스는 사이트맵 50,000개를 나열할 수 있고 같은 50 MB 제한을 받습니다. 자식 하나하나가 아니라 인덱스를 제출하고, robots.txt에서도 그것을 가리키세요.

가져오기 버튼이 제 사이트맵에서 실패한 이유는?

거의 언제나 CORS입니다. 상대 도메인이 Access-Control-Allow-Origin 헤더를 보내지 않는 한 브라우저는 이 페이지가 다른 도메인의 응답을 읽도록 허용하지 않는데, sitemap.xml에 그 헤더를 붙여 보내는 서버는 드뭅니다. 이는 여러분 서버의 정책이지 파일의 결함이 아닙니다.

프록시로 우회하지는 않습니다. 프록시를 쓰면 여러분의 URL과 파일이 우리가 운영하는 서버를 거치게 되기 때문입니다. 실패 메시지에는 입력한 주소에 대한 curl 명령이 함께 나옵니다. gzip으로 압축된 사이트맵은 먼저 풀어야 합니다. curl -sL https://example.com/sitemap.xml.gz | gunzip 처럼요.

제 사이트맵이 어딘가로 업로드되나요?

아닙니다. 파서도 모든 규칙도 이 탭에서 도는 JavaScript입니다. 무언가를 보낼 백엔드도, 편집기에 접근하는 분석 도구도 없습니다. 네트워크 패널을 열고 파일을 검사해 보세요. 페이지 자원이 한 번 로드되고 그 뒤로는 아무 일도 없습니다.

게시된 사이트맵은 공개 정보지만, 지금 디버깅 중인 파일은 그렇지 않은 경우가 많습니다. 스테이징 사이트맵에는 아직 공개하지 않은 페이지, 사내 도구, 비밀유지 계약 하의 클라이언트 사이트가 들어 있고, 이 검색어 상위 검사기 중 몇몇은 서버에서 동작합니다. 입력은 새로고침해도 잃지 않도록 이 브라우저의 localStorage에 보관되며, 지우기로 삭제됩니다.

관련 도구

참고 자료