깨진 링크를 넣어도 Astro 빌드는 성공했습니다, 배포 전 검증한 방법
깨진 내부 링크가 있어도 성공한 Astro 빌드와 별도 검증의 실패 결과를 비교하고, HTML·이미지·사이트맵을 배포 전에 검사하는 방법을 설명합니다.

Astro 블로그에서 npm run build가 성공하면 새 글이 발행 준비를 마쳤다고 생각하기 쉽습니다. 저도 처음에는 빌드 오류가 없고 생성된 페이지가 열리면 링크와 이미지까지 함께 확인됐다고 막연히 생각했습니다.
하지만 minml의 배포 과정에는 Astro 빌드 뒤에 별도의 콘텐츠 검증 단계가 있습니다. 이 검사가 실제로 필요한지 확인하기 위해 2026년 8월 29일, 기존 글의 내부 링크 하나를 로컬에서만 의도적으로 존재하지 않는 주소로 바꾸는 통제 시험을 진행했습니다.
결과는 분명했습니다. 깨진 링크가 포함된 상태에서도 Astro는 27개 페이지를 정상적으로 빌드했습니다. 그다음 실행한 별도 검증 스크립트는 해당 주소를 찾지 못하고 즉시 실패했습니다.
시험용 링크는 곧바로 원래 주소로 복구했고, 정상 빌드와 전체 검증을 다시 통과한 뒤에만 새 글 작업을 계속했습니다. 잘못된 링크가 들어간 결과물은 운영 서버에 업로드하거나 공개하지 않았습니다.
빌드 성공과 링크 정상은 서로 다른 결과였습니다
Astro 공식 배포 문서는 npm run build로 사이트를 빌드하며 기본 출력이 dist에 생성된다고 설명합니다. minml도 Markdown 글과 레이아웃을 정적 HTML로 바꾸는 이 과정을 사용합니다.
빌드는 콘텐츠 형식과 컴포넌트 코드에 문제가 없는지 확인하고 배포할 파일을 만듭니다. 그러나 Markdown에 적은 모든 내부 주소가 실제 생성 파일과 연결되는지까지 minml의 요구사항에 맞춰 확인해주지는 않았습니다.
예를 들어 다음 링크는 HTML 문법으로는 문제가 없습니다.
[관련 글](/__broken-internal-link-test__/)
해당 주소의 페이지가 없어도 링크 태그 자체는 만들 수 있습니다. 방문자가 클릭한 뒤에야 404를 만나게 됩니다. 그래서 빌드가 성공했다는 사실과 링크 대상이 존재한다는 사실을 따로 확인해야 했습니다.
이 구분은 Astro 블로그를 작성하고 배포하는 과정에서도 짧게 언급했습니다. 이번에는 그 검사에서 실제로 무엇을 확인하고, 깨진 링크를 넣었을 때 어떤 차이가 나타나는지 직접 시험했습니다.
존재하지 않는 주소로 통제 시험을 했습니다
운영 사이트나 원본 기록을 망가뜨리지 않도록 시험 범위를 한 줄로 제한했습니다.
- Git 작업 폴더가 깨끗한지 확인
- 공개 중인 기존 글의 내부 링크 한 곳만 시험 주소로 변경
- Astro 프로덕션 빌드 실행
- 별도 콘텐츠 검증 실행
- 시험 주소를 즉시 원래 내부 링크로 복구
- 정상 상태에서 빌드와 검증을 다시 실행
- Git diff가 사라졌는지 확인
깨진 링크가 들어간 빌드 결과는 다음처럼 성공했습니다.
27 page(s) built
Complete!
같은 결과물에 minml의 검증 스크립트를 실행하자 종료 코드 1과 함께 다음 오류가 나왔습니다.
Broken internal target: /__broken-internal-link-test__/
이 결과는 Astro의 결함을 발견했다는 뜻이 아닙니다. Astro 빌드와 제가 추가한 사이트 검증의 책임이 다르다는 뜻입니다. Astro는 정적 결과물을 만들었고, 별도 스크립트는 그 결과물이 minml에서 정한 연결 규칙을 만족하는지 확인했습니다.
Markdown이 아니라 생성된 HTML을 읽습니다
검증 스크립트는 먼저 콘텐츠 파일의 frontmatter에서 글 주소, 대표 이미지와 초안 여부를 읽습니다. 하지만 본문 링크는 Markdown 원문만 검색하지 않습니다. 빌드가 끝난 뒤 dist 안의 모든 HTML을 순회하며 방문자가 실제로 받을 href와 src를 수집합니다.
이렇게 한 이유는 레이아웃과 공통 메뉴에서 만들어진 링크도 함께 확인하기 위해서입니다. Markdown 본문만 검사하면 홈페이지, 작성자 페이지와 공통 내비게이션에 들어간 링크를 놓칠 수 있습니다.
현재 로직을 단순화하면 다음과 같습니다.
for (const file of htmlFiles) {
const html = await readFile(file, "utf8");
for (const match of html.matchAll(/(?:href|src)="(\/[^"]*)"/g)) {
const target = match[1];
if (target.startsWith("/content/images/")) {
imageTargets.add(target);
} else {
internalTargets.add(target);
}
}
}
Set을 사용하는 이유는 여러 페이지에 반복되는 홈페이지나 공통 이미지 경로를 한 번만 확인하기 위해서입니다. 외부 사이트 주소는 이 검사에서 제외하고 /로 시작하는 로컬 경로만 수집합니다.
URL을 실제 출력 파일로 바꿔 확인합니다
브라우저에서 보는 주소와 dist의 파일 위치는 그대로 같지 않을 수 있습니다. 예를 들어 글 주소 /example-post/는 정적 빌드에서 보통 다음 파일과 연결됩니다.
/example-post/ → dist/example-post/index.html
/content/images/a.webp → dist/content/images/a.webp
/ → dist/index.html
검증 스크립트는 쿼리 문자열과 # 뒤의 조각을 제거한 뒤 확장자 유무에 따라 출력 경로를 계산합니다.
function outputPathFromUrl(urlPath) {
const clean = decodeURIComponent(urlPath.split(/[?#]/)[0]);
if (clean === "/") return join(dist, "index.html");
if (extname(clean)) return join(dist, clean.slice(1));
return join(dist, clean.slice(1), "index.html");
}
계산한 파일이 없으면 내부 링크 또는 이미지 누락으로 처리하고 프로세스를 실패시킵니다. 오류를 경고로만 남기지 않는 이유는 이후 업로드 단계로 진행하지 못하게 하기 위해서입니다.
본문 링크 외에도 함께 확인하는 항목
깨진 링크 하나만 찾는 스크립트였다면 글이 늘어날 때마다 다른 검사를 따로 만들어야 했습니다. minml에서는 배포 결과의 구조를 한 번에 확인하도록 범위를 넓혔습니다.
공개 글과 초안
- 공개 글의 주소에 실제 HTML이 생성됐는가
draft: true인 글이 실수로 생성되지 않았는가- 공개 글은 사이트맵에 포함됐는가
- 초안은 사이트맵에서 제외됐는가
Astro 콘텐츠 컬렉션의 스키마는 글마다 필요한 frontmatter의 형식과 자료형을 확인하는 데 유용합니다. minml의 별도 검증은 그 이후 단계에서 “이 프로젝트에서 공개하기로 한 글과 감추기로 한 초안이 실제 출력에도 그대로 반영됐는가”를 확인합니다.
이미지
- 공개 글의
featureImage파일이 존재하는가 - 생성된 HTML의
/content/images/경로가 실제 파일을 가리키는가 - 본문과 메타 태그에서 반복된 같은 이미지를 중복 검사하지 않는가
이미지 경로는 페이지와 달리 확장자가 있으므로 index.html을 붙이지 않고 해당 파일 자체가 있는지 확인합니다. 대표 이미지 파일을 저장하지 않았거나 경로의 월 폴더를 잘못 쓴 경우를 배포 전에 발견할 수 있습니다.
필수 파일과 통합 주소
- 홈페이지, RSS,
robots.txt, 사이트맵이 생성됐는가 - 작성자 페이지가 존재하는가
- 기존 글을 합친 과거 주소의 리디렉션 파일이 남아 있는가
- 이전 시스템 주소를 바꾸지 못한 표시가 HTML에 남아 있지 않은가
Ghost에서 Astro로 옮기며 주소와 이미지를 확인한 과정은 Ghost에서 Astro로 글과 이미지를 옮긴 실제 과정에 정리했습니다. 처음에는 이전 누락을 찾기 위해 만든 검사였지만, 지금은 새 글을 배포할 때마다 같은 기준을 사용합니다.
시험 전후에 확인한 실제 개수
임시 링크를 원상복구한 뒤 다시 빌드했을 때의 결과는 다음과 같습니다.
| 확인 항목 | 결과 |
|---|---|
| 공개 글 | 21편 |
| 공개 제외 초안 | 10편 |
| 소개·문의·개인정보 페이지 | 3개 |
| 통합한 과거 주소의 리디렉션 | 6개 |
| 생성된 HTML | 33개 |
| 중복을 제거한 내부 경로 대상 | 31개 |
| 중복을 제거한 로컬 이미지 대상 | 22개 |
숫자가 크다고 사이트 품질이 높아지는 것은 아닙니다. 이 수치는 이전 배포와 비교했을 때 글, 링크나 이미지가 의도치 않게 빠지지 않았는지 확인하는 운영 기준입니다.
이번 시험에서 의미 있는 차이는 숫자보다 종료 결과였습니다.
| 상태 | Astro 빌드 | 별도 콘텐츠 검증 |
|---|---|---|
| 시험용 깨진 링크 포함 | 성공 | 실패 |
| 원래 내부 링크로 복구 | 성공 | 성공 |
검증이 실패하면 서버에 올리지 않습니다
minml의 배포 스크립트는 다음 순서를 따릅니다.
- 커밋하지 않은 변경이 없는지 확인
- Astro 프로덕션 빌드
- 생성된 콘텐츠와 내부 경로 검증
- 필수 파일 확인
- 새 서버 릴리스 생성과 업로드
- 권한 정규화와 공개 전환
- 실제 HTTP 응답 확인
이번에 실패한 검증은 3번입니다. 따라서 같은 깨진 링크가 실제 발행 작업 중에 들어갔다면 SSH 연결, 새 릴리스 생성과 파일 업로드 전에 작업이 중단됩니다. 운영 중인 current 링크에는 아무 변화가 없습니다.
업로드가 끝난 뒤에는 로컬 파일 존재 여부와 다른 종류의 검사가 필요합니다. 홈페이지, 새 글과 이미지가 실제로 200인지, 없는 주소가 404인지, 통합 주소가 301인지 확인합니다. 로컬 검사와 운영 응답 검사를 나눈 이유는 개인 사이트 유지보수 체크리스트에서도 확인할 수 있습니다.
자동 검사로 확인하지 못하는 것도 있습니다
현재 스크립트가 모든 링크 문제를 찾아주는 것은 아닙니다. 검사 범위를 명확히 알아야 결과를 과신하지 않을 수 있습니다.
| 확인하지 않는 항목 | 별도 확인 방법 |
|---|---|
| 외부 사이트 링크의 현재 응답 | 공식 출처를 직접 열고 발행일에 확인 |
페이지 안의 #section 조각 |
실제 제목 ID 또는 브라우저 이동 확인 |
| JavaScript가 실행 중 만드는 주소 | 기능 테스트와 브라우저 확인 |
| 링크 문구가 대상 내용을 정확히 설명하는지 | 글을 직접 읽고 문맥 검토 |
| 운영 서버의 Nginx·인증서·리디렉션 응답 | 배포 후 HTTP 검사 |
| 로그인 뒤에만 보이는 페이지 | 해당 서비스의 별도 테스트 |
특히 외부 링크를 빌드 때마다 모두 요청하면 상대 사이트의 일시적인 장애나 요청 제한 때문에 배포가 불안정해질 수 있습니다. 그래서 현재 자동 실패 조건은 프로젝트가 직접 통제하는 내부 경로와 로컬 이미지에 집중합니다.
발행 전 개인정보·보안 검토 과정도 같은 이유로 별도 단계에 남아 있습니다. 파일이 존재한다는 검사만으로 코드 블록이나 이미지 안에 공개하면 안 되는 정보가 없는지 판단할 수는 없습니다.
제가 사용하는 배포 전 경로 체크리스트
빌드 결과
- 프로덕션 빌드가 성공했는가
- 공개 글마다 해당 주소의
index.html이 있는가 - 초안이 공개 결과와 사이트맵에서 빠졌는가
- 홈페이지, RSS, robots와 사이트맵이 생성됐는가
- 이전 주소의 리디렉션 파일이 남아 있는가
링크와 이미지
- 생성된 모든 HTML에서 로컬
href와src를 수집했는가 -
/글-주소/를 실제dist/글-주소/index.html과 연결했는가 - 대표 이미지와 본문 이미지 파일이 존재하는가
- 쿼리 문자열과 URL 조각을 파일 경로와 구분했는가
- 오류가 하나라도 있으면 배포를 중단하는가
자동 검사 밖의 항목
- 외부 공식 자료가 현재도 열리고 내용이 맞는가
- 링크 문구와 연결된 글의 주제가 자연스러운가
- 모바일에서 링크를 누르기 어렵거나 겹치지 않는가
- 원문·HTML·이미지의 개인정보와 서버 정보를 검토했는가
- 배포 후 실제
200,301,404응답을 확인했는가
발행 버튼 하나 대신 실패 지점을 앞쪽으로 옮겼습니다
깨진 링크를 넣은 이번 시험에서도 Astro는 맡은 작업을 정상적으로 끝냈습니다. Markdown과 컴포넌트를 정적 파일로 만들었고 27개 페이지를 생성했습니다. 링크 대상의 존재 여부는 제가 프로젝트에 추가한 별도 검증이 찾아냈습니다.
중요한 것은 빌드 도구 하나가 모든 품질을 보장한다고 기대하는 일이 아니었습니다. 콘텐츠 형식은 Astro와 스키마가 확인하고, 생성된 내부 경로와 이미지는 별도 스크립트가 확인하며, 실제 공개 응답과 문맥·보안은 다시 다른 단계에서 확인합니다.
단계가 늘어난 것처럼 보이지만 오류를 방문자가 클릭한 뒤 발견하는 대신 업로드 전에 발견할 수 있게 됐습니다. 저에게 배포 전 검증의 목적은 모든 실수를 자동화로 없애는 것이 아니라, 기계가 확실히 판단할 수 있는 실패를 가능한 한 공개 전으로 옮기는 것입니다.