패턴

const noteModules = import.meta.glob('../content/notes/*.mdx', { eager: true });

export const getAllNotes = () =>
  Object.entries(noteModules).map(([path, mod]) => ({
    slug: extractSlug(path),
    ...(mod.frontmatter || {}),
    Component: mod.default,
  }));

새 노트 = 새 MDX 파일 1개. 컴포넌트·라우팅·인덱스 수정 0건.

왜 이게 강한가

  • 콘텐츠 작성 부담 ↓: 마크다운 + frontmatter만 알면 된다
  • 재사용 가능 컴포넌트 임베드: 본문 안에 차트·다이어그램·인터랙티브 도식
  • 빌드 타임 정적 분석: Fuse.js 검색 인덱스도 빌드 시 한 번에
  • Git diff가 깔끔: 콘텐츠 변경이 코드 변경과 섞이지 않음

함정

  • import.meta.glob은 빌드 시 평가. dev에서 HMR로 잘 따라오지만 production은 dist에 박힘.
  • eager: true로 모든 모듈 즉시 import → 콘텐츠 100개+ 되면 번들 크기 ↑. lazy 옵션 검토.
  • frontmatter 스키마 통일 안 하면 컴포넌트에서 옵셔널 체이닝 지옥.
  • 빌드 시 frontmatter 검증 누락 → 누락된 필드가 production에서야 발견.
  • 슬러그 변경 시 cross-link이 깨짐. 슬러그는 한 번 정하면 안 바꾸는 게 안전.

lazy 옵션으로 번들 분할

콘텐츠가 100개+ 되면 eager: false로 전환.

const noteModules = import.meta.glob('../content/notes/*.mdx');
// eager 없음 → 각 노트가 동적 import로 코드 분할됨

export const loadNote = async (slug) => {
  const path = `../content/notes/${slug}.mdx`;
  const mod = await noteModules[path]();
  return { slug, ...(mod.frontmatter || {}), Component: mod.default };
};

라우터의 lazy load와 결합하면 첫 페이지 로드 시 모든 노트를 import하지 않게 된다.

대가: 라우팅 시 약간의 지연 (각 노트 첫 방문 시 fetch). 단, 빌드는 가벼워지고 첫 페이지 번들이 작아진다.

한계와 전환 시그널

콘텐츠 규모별 적정 도구:

규모도구빌드 시간
~100편Vite + MDX + eager< 5초
100-500편Vite + MDX + lazy< 15초
500-2000편Next.js / Astro (SSG)30초-2분
2000편+Headless CMS + ISR점진 빌드

전환 시그널:

  • 빌드 시간 > 1분: 매 변경마다 1분 기다리면 dev loop이 깨진다.
  • frontmatter 검증 누락 사고 자주: 정적 타입 + schema 검증이 필요한 단계.
  • 콘텐츠 작성자 ≠ 개발자: 비개발자도 작성하면 CMS 인터페이스 필요.

관련

/notes/vite-manual-chunks — content/* 디렉토리별 청크 분리로 캐시 효율 ↑