lifecarelog
프론트엔드

MDX slug 중복, 빌드 깨지기 전에 잡는 30줄 검증 스크립트

MDX 프론트매터 slug가 겹치면 빌드가 깨지거나 페이지 하나가 조용히 사라져요. Node 기본 모듈만으로 30줄 안에 빌드 전 중복을 잡는 스크립트를 정리했어요.

7분 읽기

블로그를 MDX로 운영하면 글 한 편이 파일 하나예요. 프론트매터에 적은 slug가 그대로 주소가 되고요. 여기서 slug 두 개가 겹치면 어떻게 될까요. 프레임워크와 로더 구현에 따라 빌드가 실패하기도 하고, 더 나쁘게는 아무 말 없이 한 페이지가 다른 페이지를 덮어써요. 목록 화면에는 카드가 두 장 보이는데 둘 다 같은 글로 가는 거죠. 독자 입장에서는 링크가 고장 난 것과 다를 게 없어요.

라이프케어로그 블로그도 content 폴더 아래에 MDX를 쌓고 있어요. 플랜씨 금융계산기처럼 한 주제로 글이 여러 편 나오면 비슷한 slug가 자연스럽게 생겨요. 그래서 빌드 앞단에 짧은 검증 스크립트를 하나 두고 있어요.

MDX 파일 순회프론트매터에서 slug 추출Map으로 중복 확인겹치면 종료 코드 1

화면에서 먼저 보이는 증상

동적 라우트 [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줄 안에서 필요한 일을 다 하고 있어요.

#MDX#Next.js#빌드검증

라이프케어로그 서비스가 궁금하신가요?

AI 기반 건강·일정·재활 관리 앱을 직접 써보세요.

서비스 살펴보기

관련 글

댓글

아직 댓글이 없어요. 첫 댓글을 남겨주세요.