아직 배포 전이라면 로컬 파일 검사 실습부터 시작하세요. 이 글은 파일이 준비된 다음, 실제 주소에서 어떤 문서가 도착하는지 확인하는 단계입니다.
1. 페이지 소스와 화면은 언제 달라질까?
서버가 처음 보내 준 HTML과 브라우저가 JavaScript를 실행한 뒤 만든 문서는 서로 다를 수 있습니다. 예를 들어 처음에는 아래의 빈 요소만 오고, 앱 코드가 실행되면서 그 안에 제목과 글을 넣을 수 있습니다.
<!-- 설명을 위한 단순 예시: 서버가 보낸 본문 -->
<main id="app"></main>
<script src="/app.js"></script>
<!-- JavaScript 실행 뒤 화면의 문서 -->
<main id="app">
<h1>배포가 끝난 뒤 확인할 것</h1>
<p>주소, 본문, 상태 코드를 따로 확인합니다.</p>
</main>
Chrome에서 해당 페이지를 연 뒤 마우스 오른쪽 버튼 → 페이지 소스 보기로 최초 HTML을 읽어 보세요. 같은 페이지에서 F12 → Elements를 열면 스크립트 실행 후의 문서를 볼 수 있습니다. 제목만 비교하지 말고 본문의 고유한 문장과 다음 글로 가는 링크까지 비교해야 차이를 발견하기 쉽습니다.
Google 검색은 JavaScript를 실행할 수 있으므로 “빈 초기 HTML이면 무조건 검색 불가”는 아닙니다. 다만 스크립트나 API 요청이 실패하면 본문도 사라지고, JavaScript를 실행하지 않는 도구에는 읽을거리가 전달되지 않습니다. 정적 렌더링은 이 의존성을 줄이는 선택입니다. Google의 JavaScript 처리 과정 설명도 최초 수집과 렌더링을 구분합니다.
2. 두 URL의 응답을 직접 비교하기
아래 명령은 공개 문서를 읽기만 합니다. Windows의 시작 → 터미널 → PowerShell 탭에서 실행합니다. 먼저 같은 사이트의 목적이 다른 페이지를 고릅니다. 여기서는 제작 노트 목록과 프로젝트 목록을 사용합니다.
$notesPage = Invoke-WebRequest -UseBasicParsing 'https://jjunybox.com/notes'
$projectsPage = Invoke-WebRequest -UseBasicParsing 'https://jjunybox.com/projects'
$notesPage.StatusCode
$notesPage.Headers['Content-Type']
$notesPage.Content.Contains('제작 노트')
$notesPage.Content.Contains('/note-cloudflare-static-routes')
$notesPage.Content -eq $projectsPage.Content
현재 정적 구조에서 기대하는 순서는 200, text/html을 포함한
형식, True, True, 마지막은 False입니다.
마지막 값은 두 응답이 서로 다른 문서인지 검사합니다. 예상값이며 본문 검증을
대체하는 합격 판정은 아닙니다. 네트워크 오류가 나면 먼저 URL이 열리는지 확인하세요.
단순한 문서 차이는 요청마다 바뀌는 식별자 하나로도 생깁니다. 반대로 같은 제목은 공통 메뉴에도 있습니다. 그래서 제목·H1·본문의 고유한 문장·내부 링크를 함께 찾습니다. 자신의 사이트에서는 위 검색 문구를 본문에 실제로 있는 문장으로 바꾸세요.
# H1과 canonical 태그만 골라 보기
$notesPage.Content | Select-String -Pattern '<h1[^>]*>.*?</h1>','<link[^>]*canonical[^>]*>' -AllMatches |
ForEach-Object { $_.Matches.Value }
이 정규식은 한 줄로 작성된 간단한 태그를 빠르게 확인하는 보조 수단입니다. 태그가 여러 줄로 나뉘거나 속성 형태가 다르면 놓칠 수 있습니다. 결과가 없을 때는 앞 단계의 페이지 소스 보기에서 직접 찾고, 대규모 검사는 HTML 파서를 사용합니다.
실험: ‘제작 노트’라는 글자를 찾았는데 본문은 없었습니다
응답에 페이지 이름이 있다는 것만 확인하면 공통 메뉴 때문에 검사가 통과할 수 있습니다. 이 차이를 보려고 세 개의 작은 HTML 조각을 만들었습니다. 모두 메뉴에는 ‘제작 노트’가 있지만, 첫 번째만 요청한 글의 고유 본문을 담습니다. 두 번째는 앱이 채울 빈 영역, 세 번째는 요청과 다른 홈 본문입니다.
Python 3가 설치된 환경에서 VS Code → 새 텍스트 파일에 아래 코드를 붙여넣고 initial-body.py로 저장합니다. Terminal → New Terminal에서 해당 폴더의 py -3 initial-body.py를 실행합니다. macOS·Linux에서는 python3 initial-body.py를 사용할 수 있습니다. 실제 사이트에 요청하지 않는 독립 실험입니다.
from html.parser import HTMLParser
class MainText(HTMLParser):
def __init__(self):
super().__init__()
self.in_main = False
self.parts = []
def handle_starttag(self, tag, attrs):
if tag == "main":
self.in_main = True
def handle_endtag(self, tag):
if tag == "main":
self.in_main = False
def handle_data(self, data):
if self.in_main:
self.parts.append(data)
menu = '<nav><a href="/notes">제작 노트</a></nav>'
sentence = "배포 경로와 실제 본문을 함께 확인합니다."
samples = {
"static": menu + '<main><h1>배포 점검</h1><p>' + sentence + '</p></main>',
"shell": menu + '<main id="app"></main><script src="/app.js"></script>',
"fallback": menu + '<main><h1>환영합니다</h1><p>홈 소개입니다.</p></main>',
}
for name, source in samples.items():
parser = MainText()
parser.feed(source)
has_body = sentence in " ".join(parser.parts)
assert ("제작 노트" in source) is True
assert has_body == (name == "static")
print(f"{name}: menu=True, unique_body={has_body}")
static: menu=True, unique_body=True
shell: menu=True, unique_body=False
fallback: menu=True, unique_body=False
두 실패의 수정 위치는 다릅니다
shell은 렌더링 후에 글이 생길 수 있습니다. JavaScript 실행과 데이터 요청을 확인하고, 공개 글을 최초 응답에도 담을지 결정합니다. fallback은 이미 글이 있지만 다른 페이지의 글입니다. 프리렌더를 추가하기 전에 요청 경로와 실제 배포 파일이 맞는지 봐야 합니다.
이 작은 파서는 알려진 예제에서 main 안의 문자만 비교합니다. 숨김 요소, 스크립트 문자열, 접근성 트리나 완전한 HTML 유효성을 판정하는 검사기가 아닙니다. 실서비스에서는 의미 있는 본문 문장·H1·canonical·응답 상태와 렌더링 화면을 함께 확인합니다. 두 실패가 같은 200 응답으로 도착할 수도 있으므로, HTTP 성공과 원하는 문서 전달을 따로 기록해야 합니다.
3. 결과에 따라 고칠 위치가 다릅니다
- 화면에는 글이 있는데 최초 응답에는 없다
- JavaScript 렌더링 의존성을 확인합니다. F12 → Network에서 문서 요청을 선택하고 Response를 읽습니다. 이어 Console의 스크립트 오류와 Network의 API 실패를 봅니다. 공개 본문은 정적 생성이나 서버 렌더링으로 옮길 후보입니다.
- 서로 다른 주소가 홈의 같은 본문을 돌려준다
- 라우팅 또는 SPA 폴백을 먼저 확인합니다. 존재하는 경로의 정적 파일이 빠졌는지, 없는 경로까지 홈에 연결하는 규칙이 있는지 확인하세요. 경로와 404 실습에서 상태 코드까지 함께 검사합니다.
- 본문은 다른데 canonical이 모두 홈이다
- 콘텐츠 전달과 별개로 메타데이터를 고쳐야 합니다. 각 문서가 자기 대표 URL을 가리키도록 하고, 공통 템플릿의 홈 주소가 복제되지 않았는지 살펴봅니다.
- 로컬에는 새 글이 있지만 운영 응답에는 없다
- Cloudflare Dashboard → Workers & Pages → 해당 프로젝트 → Deployments에서 배포 커밋을 확인합니다. Settings의 빌드 설정에서 게시 폴더도 대조합니다. 다른 폴더를 배포했다면 캐시를 지우는 것만으로는 고쳐지지 않습니다.
쭈니박스에서 확인한 문제와 선택
2026년 8월 아케이드의 내부 운영 기록에는 여러 URL이 동일한 2,020바이트의 초기 HTML을 반환했다는 관찰이 남아 있습니다. 이를 줄이기 위해 아케이드는 빌드 시 공개 페이지를 미리 렌더링하는 방식을 도입했습니다.
루트 사이트는 각 글을 HTML 파일에 직접 적는 방식을 유지했습니다. 이 글도 앱 실행이나 API 응답을 기다리지 않고 본문을 전달합니다. 초기 HTML 결함을 고쳤다는 사실과 광고 심사 결과는 별개입니다. 당시 반려의 단일 원인이 HTML 전달 방식이었다고 확인된 것은 아닙니다.
4. 공개 글은 어떻게 만들어 전달할까?
기준은 사용하는 프레임워크의 이름보다 콘텐츠가 언제 바뀌는지입니다. 글을 수정할 때만 내용이 바뀐다면, 매 요청마다 서버가 같은 글을 다시 만들 필요는 적습니다. 반대로 사용자마다 다른 비공개 기록을 공개 HTML로 생성하면 안 됩니다.
- 평면 HTML: 지금 이 사이트처럼 페이지를 직접 관리할 수 있을 때 간단합니다. 공통 메뉴 수정이 여러 파일에 걸리는 유지 비용은 감수해야 합니다.
- 정적 생성·프리렌더: 공개 경로가 많거나 기존 React 앱을 유지할 때 검토합니다. 글이 바뀌면 다시 빌드해야 하고, 생성 대상에서 빠진 경로와 렌더링 중 API 실패를 검사해야 합니다.
- 서버 렌더링: 요청 시점의 최신 공개 데이터가 꼭 필요할 때 후보가 됩니다. 서버 실행 환경과 캐시, 장애 처리까지 운영할 책임이 늘어납니다.
- 클라이언트 렌더링: 로그인 후 편집 화면이나 게임 조작처럼 상호작용이 중심이면 유지할 수 있습니다. 검색 대상 안내문과 앱 동작을 구분하세요.
이 사이트의 선택을 모든 프로젝트에 그대로 적용할 필요는 없습니다. 같은 도메인에서도 공개 안내 글과 로그인 앱의 렌더링 요구는 다를 수 있습니다.
5. 본문이 도착한 뒤에도 남는 확인
명령으로 본문을 찾았으면 브라우저에서 글 전체를 읽고 주요 링크를 따라가 봅니다. 스크립트가 꺼진 상태에서도 요지를 이해할 수 있는지, 모바일에서 예제가 잘리지 않는지 확인합니다. 글을 전달할 수 있다는 것과 독자에게 유용하다는 것은 다른 평가입니다.
Google에 전달된 문서를 확인하려면 Search Console → 해당 사이트 속성 → 상단 URL 검사 → 실제 URL 테스트 → 테스트한 페이지 보기 → HTML을 사용합니다. 이 결과는 Google 검색의 검사이며 AdSense 승인 여부를 대신 판정하지 않습니다.
정상 페이지·리디렉션·404를 구분하는 실습으로 응답 검사를 마무리하거나, 사이트 이전 전에 확인할 항목으로 이어갈 수 있습니다.