이 프로젝트는 React Router 7(SSR·프리빌드) 위에서 **mdx-bundler**로 MDX 파일을 빌드해, 로더에서 codefrontmatter를 받은 뒤 화면에서 getMDXComponent로 렌더링합니다. Vite 플러그인으로 MDX를 직접 import 하지 않고, 파일 경로만 지정해 서버에서 번들링하는 방식입니다.

1. 의존성과 역할

  • **package.json**의 dependenciesmdx-bundler가 포함되어 있습니다. 별도의 @mdx-js/react Vite 플러그인은 이 경로에서는 사용하지 않습니다.
  • mdx-bundler는 내부적으로 @mdx-js/mdx/esbuild 등을 묶어, MDX 문자열 또는 파일 경로(file) 기준으로 컴파일합니다.
  • 기본 설정으로 **remark-mdx-frontmatter**에 해당하는 처리가 포함되어 YAML frontmatter를 메타데이터로 분리합니다(프로젝트 코드에서 별도 remark 플러그인을 넘기지 않음).

2. MDX 파일을 두는 위치

콘텐츠는 기능(feature)별 docs 폴더에 둡니다.

영역MDX 디렉터리예시 URL
법무· 정책app/features/legal/docs//legal/:slug (파일명 privacy-policy.mdx → slug privacy-policy)
블로그app/features/blog/docs//blog/:slug (파일명 hello-world.mdx → slug hello-world)

파일 이름이 URL slug와 1:1로 대응합니다. 로더는 process.cwd() 기준으로 path.join(process.cwd(), "app", "features", …, "docs", "<slug>.mdx") 형태로 파일을 찾습니다. 여기서 <slug>는 URL 파라미터와 동일한 파일명(확장자 제외)을 뜻합니다.

3. 라우팅 등록 (app/routes.ts)

동적 페이지는 라우트에 이미 정의되어 있습니다.

  • 법무: ...prefix("/legal", [route("/:slug", "features/legal/screens/policy.tsx")])
  • 블로그 목록·상세: /blog, /blog/:slug

**새 “섹션”**을 MDX로 추가하려면 docs 디렉터리, 화면(로더/getMDXComponent), 그리고 여기 같은 routes.ts 항목을 같은 패턴으로 추가하면 됩니다.

4. 화면에서의 공통 패턴

대표적으로 app/features/legal/screens/policy.tsxapp/features/blog/screens/post.tsx가 같은 흐름을 따릅니다.

  1. 로더에서 bundleMDX({ file: filePath }) 호출 → code(컴파일 결과 문자열)·frontmatter 획득.
  2. 파일이 없으면 ENOENT 등으로 처리하고 404/500(프로젝트에서는 throw data(null, { status }) 패턴)을 반환합니다.
  3. 기본 컴포넌트에서 const MDXContent = getMDXComponent(code)<MDXContent components={{ … }} />로 출력합니다.
  4. components 맵으로 마크다운 요소를 앱 디자인에 맞는 래퍼로 바꿉니다. 공통 내용은 ~/core/components/mdx-typographyTypographyH1, TypographyP, TypographyInlineCode 등을 사용합니다.

블로그 목록(app/features/blog/screens/posts.tsx)은 각 .mdx에 대해 bundleMDX만 호출하고 frontmatter 모아 카드 목록으로 씁니다(본문 code는 쓰지 않음).

5. Frontmatter 규약

법무 페이지는 최소 title, **description**을 기대합니다. meta에서 제목·설명 메타태그에 넣습니다.

블로그 포스트는 예시 파일(hello-world.mdx)처럼 아래 필드를 함께 두는 것이 좋습니다.

  • title, description, date, category, author, slug
    목록 화면·정렬·검증·OG 라우트가 이 값들을 참고합니다.

6. MDX 안에서 React 컴포넌트 쓰기

6.1. 이 레포에서 꼭 알아야 할 점 (components 맵)

getMDXComponent(code)로 만든 MDX 출력 안에서는 대문자로 시작하는 커스텀 태그(예: Button)가, 같은 파일 상단에서 import 했더라도 렌더할 때 넘기는 components prop에서 그 이름으로 전달된 컴포넌트와 먼저 연결되는 방식입니다. import만 하고 해당 라우트 화면의 components에 빠져 있으면 Expected component 'Button' to be defined … 같은 오류가 납니다.

그래서 앱 UI를 쓰려면 policy.tsx(법무)나 post.tsx(블로그)에서 MDXContent에 넘기는 components 객체에 해당 컴포넌트를 추가하세요. 법무 MDX에서 Button을 쓰려면 policy.tsx의 맵에 Button 항목이 있어야 합니다.

6.2. feature 안 상대 경로 import (블로그 예시)

블로그 문서에서는 feature 내부 컴포넌트를 상대 경로로 가져와 쓰는 예시가 있습니다.

import CounterExample from "../components/counter-example";

<CounterExample />

esbuild가 경로를 해석할 수 있도록 상대 경로를 맞추고, 해당 컴포넌트가 MDX 렌더 환경에서 안전하게 동작하는지 확인합니다.

6.3. 앱 공용 컴포넌트 (~/…)를 MDX에서 쓰는 경우

~/core/components/ui/button처럼 경로 별칭으로 import 한 뒤 <Button>을 쓸 수 있습니다. 이때도 반드시 해당 라우트 화면의 components.Button 등록을 맞춰 두세요(§6.1).

아래 버튼은 policy.tsxButton을 넘겼을 때만 정상 렌더됩니다.

7. 프리렌더(정적 생성) 설정 (react-router.config.ts)

  • 블로그 글 URL은 빌드 시 app/features/blog/docs 디렉터리를 스캔해 /blog/[파일명-확장자제거]prerender 목록에 자동으로 포함합니다.
  • 법무 경로는 예시상 /legal/terms-of-service, /legal/privacy-policy처럼 명시적으로 나열되어 있습니다. legal/docs에 새 MDX를 추가하고 정적 HTML로 미리 렌더하고 싶다면, prerender() 반환 배열에 해당 경로를 직접 추가해야 합니다.

8. 사이트맵·기타 소비처

app/core/screens/sitemap.tsblog/docslegal/docs.mdx 파일명으로 각각 /blog/..., /legal/... URL을 만들어 포함합니다. MDX 파일만 추가해도 사이트맵 엔트리는 자동으로 늘어납니다(SITE_URL 등 환경 변수 전제는 동일).

블로그 공유 미리보기용 /api/blog/og(app/features/blog/api/og.tsx)도 slug에 해당하는 MDX에서 bundleMDXfrontmatter만 읽어 이미지를 생성합니다.

9. 도구 및 포매팅

  • npm run format 스크립트에 *.mdx가 포함되어 Prettier로 정리할 수 있습니다.

10. 새 콘텐츠 추가 체크리스트 요약

법무 MDX 추가

  1. app/features/legal/docs/<slug>.mdx 생성 및 frontmatter(title, description) 작성.
  2. 브라우저에서 /legal/<slug>로 접근 확인.
  3. 프리렌더가 필요하면 react-router.config.tsprerender()에 해당 URL 추가.

블로그 MDX 추가

  1. app/features/blog/docs/<slug>.mdx 생성 및 블로그용 frontmatter·본문 작성.
  2. 카드 이미지를 쓰는 경우 공개 자산 규약에 맞게 /blog/<slug>.jpg(화면에서 사용하는 경로) 배치 여부 확인.
  3. 라우트·프리렌더는 docs 스캔으로 블로그 글 경로가 자동 반영되는 구조입니다.

주의: MDX 본문에서 중괄호로 변수 이름을 적는 형태는 자바스크립트 표현식으로 컴파일됩니다. 문자열 안에 예시 코드를 적을 때도 이런 문법을 섞어 두면(예를 들어 역따옴표 문자열 속에 템플릿 리터럴을 넣은 경우) slug is not defined 같은 런타임 오류로 페이지 전체가 실패할 수 있으므로, 플레이스홀더는 이 문서의 §2처럼 "<slug>.mdx"처럼 표현하거나 fenced 코드 블록 안에만 두세요.

위 항목을 지키면 이 레포 안에서 MDX 세팅과 사용처(정책 상세·블로그 목록·OG·사이트맵)와 일관되게 동작합니다.