MDX slug 중복, 빌드 깨지기 전에 잡는 30줄 검증 스크립트
MDX 프론트매터 slug가 겹치면 빌드가 깨지거나 페이지 하나가 조용히 사라져요. Node 기본 모듈만으로 30줄 안에 빌드 전 중복을 잡는 스크립트를 정리했어요.
블로그를 MDX로 운영하면 글 한 편이 파일 하나예요. 프론트매터에 적은 slug가 그대로 주소가 되고요. 여기서 slug 두 개가 겹치면 어떻게 될까요. 프레임워크와 로더 구현에 따라 빌드가 실패하기도 하고, 더 나쁘게는 아무 말 없이 한 페이지가 다른 페이지를 덮어써요. 목록 화면에는 카드가 두 장 보이는데 둘 다 같은 글로 가는 거죠. 독자 입장에서는 링크가 고장 난 것과 다를 게 없어요.
라이프케어로그 블로그도 content 폴더 아래에 MDX를 쌓고 있어요. 플랜씨 금융계산기처럼 한 주제로 글이 여러 편 나오면 비슷한 slug가 자연스럽게 생겨요. 그래서 빌드 앞단에 짧은 검증 스크립트를 하나 두고 있어요.
화면에서 먼저 보이는 증상
동적 라우트 [slug] 페이지는 slug 목록을 받아 정적 페이지를 찍어내요. 같은 값이 두 번 들어가면 어느 쪽이 이기는지는 구현마다 달라요. 문제는 그걸 깨닫는 시점이 배포 후가 되기 쉽다는 점이에요. 글 목록 컴포넌트는 파일 수만큼 카드를 렌더링하니까 겉으로는 멀쩡해 보이거든요. 카드를 눌러 봐야 엉뚱한 글이 열리는 걸 알아요.
그래서 검사는 화면이 아니라 파일 단계에서 끝내야 해요. 의존성 0개, 정규식 2개면 충분했어요.
스크립트
// scripts/check-slugs.mjs
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
const ROOT = "content";
const seen = new Map(); // slug -> file path
function walk(dir) {
for (const name of readdirSync(dir)) {
const path = join(dir, name);
if (statSync(path).isDirectory()) walk(path);
else if (name.endsWith(".mdx")) check(path);
}
}
function check(file) {
const src = readFileSync(file, "utf8");
const block = src.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1] ?? "";
const slug = block.match(/^slug:\s*["']?([^"'\r\n]+)["']?/m)?.[1]?.trim();
if (!slug) return console.warn(`slug 없음: ${file}`);
const prev = seen.get(slug);
if (!prev) return seen.set(slug, file);
console.error(`중복 slug "${slug}"\n ${prev}\n ${file}`);
process.exitCode = 1;
}
walk(ROOT);
if (!process.exitCode) console.log(`OK: slug ${seen.size}개 검사 완료`);직접 만들어 보니 프론트매터 파서를 따로 들일 이유가 없었어요. 파일 맨 앞의 --- 블록 하나만 읽으면 되고, slug 줄은 따옴표가 있든 없든 잡히게 해 뒀어요. 핵심은 Map이에요. slug를 키로, 처음 본 파일 경로를 값으로 넣어 두면 두 번째로 같은 slug가 나올 때 이전 경로를 바로 꺼낼 수 있어요. 그래서 에러 메시지에 파일 경로 2개가 같이 찍혀요. 어느 글을 고칠지 찾아다닐 필요가 없어요.
process.exit(1)이 아니라 process.exitCode = 1을 쓴 것도 의도예요. 중간에 끊지 않고 끝까지 돌아서 중복을 한 번에 다 보여 주거든요.
빌드 앞에 붙이기
package.json의 빌드 명령 앞에 &&로 이어 두면 끝이에요.
{
"scripts": {
"check:slugs": "node scripts/check-slugs.mjs",
"build": "node scripts/check-slugs.mjs && next build"
}
}종료 코드가 1이면 && 뒤는 실행되지 않으니 빌드 자체가 시작을 안 해요. 배포 파이프라인에서도 같은 명령을 쓰니까 로컬과 CI 결과가 달라질 일이 없어요. 커밋 훅에 check:slugs를 걸어 두면 한 단계 더 앞에서 잡히고요.
일부러 깨뜨려 보기
테스트해 보면 확신이 생겨요. 아무 글의 slug를 복사해서 다른 파일에 붙여 넣고 돌려 보니 두 경로가 나란히 출력되고 빌드가 멈췄어요. 원래대로 되돌리면 OK와 함께 검사한 slug 개수가 찍혀요. slug 줄이 아예 없는 초안은 경고만 내고 통과시키게 했는데, 이 부분은 팀 규칙에 따라 실패로 바꿔도 돼요.
적용해 봤더니 마음이 편해진 건 결과가 3단계로 단순하다는 점이에요. 통과, 경고, 실패. 그 이상 판단할 게 없어요.
- 의존성 0개
- 정규식 2개
- 중복 시 파일 경로 2개 출력
- 종료 코드 1로 빌드 차단
한 줄씩 더 붙일 만한 것
이 블로그는 slug를 영문 20~50자로 잡고 있어요. 길이 검사나 허용 문자 검사는 check 함수 안에 조건 한 줄이면 들어가요. 콘텐츠 루트가 여러 개라면 ROOT를 배열로 바꾸고 walk를 돌리면 되고요. 프론트매터가 복잡해져서 정규식이 버거워지는 날이 오면 그때 파서를 들이면 돼요. 지금은 30줄 안에서 필요한 일을 다 하고 있어요.
관련 글
댓글
아직 댓글이 없어요. 첫 댓글을 남겨주세요.