Tech
2026.07.10 (최종 수정일 2026.08.30)

FE | MDX(Markdown with JSX) 문서에 대해 알아보기

#mdx#blog#fe#frontend

MDX란 무엇인가?

MDX의 개념

MDX는 Markdown with JSX의 약자로, 마크다운 문서 안에 JSX 컴포넌트를 직접 삽입할 수 있게 해주는 포맷입니다. 파일 확장자는 .mdx를 사용합니다.

마크다운은 텍스트 중심의 정적인 문서를 빠르게 작성하는 데 특화되어 있고, JSX는 자바스크립트에서 XML과 유사한 문법으로 UI를 정의하는 문법입니다. MDX는 이 둘을 합쳐 콘텐츠 작성과 동적인 인터페이스 구성을 한 파일 안에서 처리합니다.

장점과 대가

장점

  • 확장성 — 일반 마크다운으로는 불가능한 UI 요소를 문서에 포함할 수 있습니다.
  • 재사용성 — 컴포넌트로 만들어 두면 여러 글에서 같은 표현을 반복해 쓸 수 있습니다.
  • 유연성 — 콘텐츠와 UI를 한곳에서 관리하므로 유지보수 지점이 흩어지지 않습니다.

대가

  • 파싱 규칙이 엄격합니다. 마크다운에서는 그냥 텍스트였던 <, { 가 JSX 문법으로 해석되어 빌드가 실패할 수 있습니다. 자세한 사례는 자주 겪는 파싱 에러에 정리했습니다.
  • 컴포넌트가 주입되어 있어야 합니다. 문서에 쓴 컴포넌트가 렌더러에 등록되어 있지 않으면 글 전체가 렌더링에 실패합니다.

기본 문법

인라인 서식

작성법결과
*기울임*기울임
**굵게**굵게
***굵고 기울임***굵고 기울임
~~취소선~~취소선
`인라인 코드`인라인 코드
[링크](https://...)링크

제목

작성법

## H2 — 대주제
 
### H3 — 중주제
 
#### H4 — 소주제

결과

이 문서에서 보이는 모든 절 제목이 그 결과입니다. 여기서 실제 제목을 예시로 넣으면 목차에 섞여 들어가기 때문에 렌더링 예시는 생략합니다.

이 블로그의 목차는 H2 ~ H4 만 자동 수집합니다.

수준목차 등록용도
#✗글 제목 자리 (본문 사용 금지)
##✓대주제
###✓중주제
####✓소주제
##### 이하✗스타일이 정의돼 있지 않아 본문과 구분되지 않음

제목은 코드블록 안에 있으면 목차에 수집되지 않습니다. 목차 추출이 정규식이 아니라 remark AST를 순회하는 방식이라, 코드블록 안의 #은 애초에 제목 노드가 아니기 때문입니다.

리스트

작성법

- 순서 없는 항목
- 두 번째 항목
  - 하위 항목 (공백 2칸 들여쓰기)
 
1. 순서 있는 항목
2. 두 번째 항목
   1. 하위 항목 (공백 3칸 들여쓰기)

결과

  • 순서 없는 항목
  • 두 번째 항목
    • 하위 항목 (공백 2칸 들여쓰기)
  1. 순서 있는 항목
  2. 두 번째 항목
    1. 하위 항목 (공백 3칸 들여쓰기)

인용

작성법

> 인용문구
>
> > 중첩된 인용문구

결과

인용문구

중첩된 인용문구

표

작성법

| 헤더1   | 헤더2   |
| ------- | ------- |
| 데이터1 | 데이터2 |

결과

헤더1헤더2
데이터1데이터2

표가 화면보다 넓어지면 페이지 전체가 밀리는 대신 표 안에서만 가로 스크롤이 생깁니다.

코드블록

작성법

```bash
echo "언어를 명시하면 구문 강조가 적용됩니다"
```

결과

echo "언어를 명시하면 구문 강조가 적용됩니다"

언어를 명시하지 않으면 강조 없이 단색으로 출력됩니다. 로그 원문처럼 강조가 오히려 방해가 되는 경우에는 text를 지정합니다.

구문 강조는 rehype-pretty-code + shiki로 처리하며, 라이트/다크 테마 색상을 양쪽 모두 미리 심어두고 CSS로 전환합니다. 테마를 바꿔도 다시 계산하지 않고, 브라우저로 나가는 하이라이팅용 자바스크립트도 없습니다.

코드블록 부가 기능

언어 뒤에 옵션을 덧붙여 쓸 수 있습니다. 순서는 상관없고 섞어 써도 됩니다.

쓰고 싶은 것문법
파일명 표시title="lib/content/posts.ts"
특정 줄 강조{2,4-5} — 낱개는 쉼표, 범위는 하이픈
특정 단어 강조/함수이름/
줄번호showLineNumbers

작성법

```ts title="lib/content/posts.ts" {2,4-5}
const one = 1;
const two = 2;
const three = 3;
const four = 4;
const five = 5;
```

결과

lib/content/posts.ts
const one = 1;
const two = 2;
const three = 3;
const four = 4;
const five = 5;

작성법

```ts showLineNumbers /two/
const one = 1;
const two = 2;
```

결과

const one = 1;
const two = 2;

자동으로 붙는 것

따로 쓸 필요 없이 모든 코드블록에 붙습니다.

  • 복사 버튼 — 우상단에 항상 표시됩니다. 호버할 때만 나타나게 하면 터치 기기에서 누를 수가 없어 그렇게 두었습니다. 줄번호를 켜도 번호는 복사되지 않습니다(CSS 카운터로 그려서 텍스트가 아니기 때문입니다).
  • 길면 접기 — 높이가 600px를 넘으면 접히고 "코드 전체보기" 버튼이 생깁니다.

MDX 고유 문법

변수와 표현식

중괄호 안에서 자바스크립트 표현식을 평가할 수 있습니다.

export const year = new Date().getFullYear();
 
올해는 {year}년입니다. 간단한 연산도 됩니다: {2 + 3}

컴포넌트 삽입

import CustomButton from "../components/CustomButton.jsx";
 
<CustomButton />

다만 이 블로그는 글에서 직접 import 하는 방식을 쓰지 않습니다. 사용 가능한 컴포넌트는 렌더러에 미리 주입되어 있으며, 그 목록은 다음 장에 정리했습니다.

마크다운과 다른 점

항목마크다운MDX
단일 태그<br> 허용<br /> 처럼 반드시 닫아야 함
< { 문자그냥 텍스트JSX로 해석 → 이스케이프 필요
속성명class, forclassName, htmlFor (카멜케이스)

이 블로그에 적용된 컴포넌트

한눈에 보기

컴포넌트무엇을 하나어떻게 쓰나
MdxImage이미지를 next/image로 최적화자동 — ![alt](src) 그대로
CollapsiblePre긴 코드블록 접기자동 — 코드블록 그대로
LinkPreview외부 링크를 카드로 표시직접 — 태그를 써야 함
PdfEmbedPDF 문서 카드 + 미리보기직접 — 태그를 써야 함

앞의 둘은 평소처럼 마크다운을 쓰면 알아서 적용되고, 뒤의 둘만 직접 작성합니다.

자동 ① 이미지 최적화 (MdxImage)

마크다운 이미지 문법이 next/image로 바뀌어 렌더링됩니다.

작성법

![대체 텍스트](https://placehold.co/600x300/22c55e/fff?text=MdxImage)

결과

대체 텍스트

next/image는 원칙적으로 이미지의 가로·세로 크기를 알아야 하는데 마크다운 문법에는 그 정보가 없습니다. 그래서 컴파일 단계에서 rehype 플러그인이 파일을 한 번 읽어 실제 픽셀 치수를 재고 그 값을 컴포넌트에 넘깁니다. 렌더링할 때마다 다시 재지 않습니다.

두 경우에는 최적화 없이 평범한 <img>로 넘어갑니다. 글이 깨지는 것보다 낫기 때문입니다.

상황동작
치수를 알아내지 못함 (파일 없음·네트워크 실패)평범한 <img>
허용 목록에 없는 외부 호스트평범한 <img>

허용된 외부 호스트는 i.scdn.co(Spotify 앨범 커버)와 placehold.co 두 곳입니다. 그 외 호스트 (예: 구글 드라이브 이미지)는 최적화 대상이 아니라 원본 그대로 나갑니다.

자동 ② 긴 코드블록 접기 (CollapsiblePre)

코드블록의 높이가 600px 을 넘으면 자동으로 접히고 하단에 "전체 보기" 버튼이 생깁니다. 작성 시 따로 할 일은 없으며, 로그 원문처럼 긴 블록을 붙여넣어도 글의 흐름이 끊기지 않습니다.

직접 ① 외부 링크 카드 (LinkPreview)

참고 링크를 제목·설명·썸네일이 있는 카드로 보여줍니다.

작성법

<LinkPreview url="https://github.com/mdx-js/mdx" />

결과

GitHub - mdx-js/mdx: Markdown for the component eraMarkdown for the component era. Contribute to mdx-js/mdx development by creating an account on GitHub.GitHub

대상 사이트의 OG 태그를 읽어 카드를 만듭니다. 서버 컴포넌트라 빌드 시점에 한 번만 외부 사이트를 호출하며, 방문자가 볼 때마다 다시 부르지 않습니다.

폴백 동작

OG 태그를 가져오지 못하면(사이트 다운, 5초 타임아웃 초과, 봇 차단 페이지, OG 태그 자체가 없는 사이트) 호스트명만 담은 간단한 카드로 폴백합니다. 죽은 링크 하나 때문에 글 전체가 실패하지 않도록 한 처리입니다.

같은 문법으로 OG 태그가 없는 주소를 넣으면 이렇게 됩니다.

작성법

<LinkPreview url="https://mdxjs.com/" />

결과 — MDX 공식 사이트에는 OG 태그가 없어 카드가 만들어지지 않습니다.

mdxjs.com

<title> 태그로는 폴백하지 않습니다. 봇 차단 페이지는 OG 태그 없이 <title>만 있는 경우가 많아, 그대로 받으면 "Just a moment..." 같은 안내문이 카드 제목으로 올라오기 때문입니다.

직접 ② PDF 문서 (PdfEmbed)

발표 자료나 산출물 같은 PDF를 카드로 보여줍니다.

작성법

<PdfEmbed src="/docs/2026-capstone-final.pdf" title="캡스톤 최종 발표자료" />

파일은 public/ 아래에 두고 그 기준 경로를 적습니다(public/docs/foo.pdf → /docs/foo.pdf).

표시되는 것

용량과 페이지 수는 적지 않습니다. 빌드할 때 파일을 직접 열어 읽습니다 — 본문에 손으로 적어두면 파일을 갈아끼울 때 숫자만 남아 어긋나기 때문입니다.

[문서]  캡스톤 최종 발표자료
        PDF · 7쪽 · 4.2 MB
        [미리보기]  [새 탭에서 열기]

동작

  • 평소에는 PDF를 내려받지 않습니다. 카드만 그려지고, "미리보기"를 눌러 펼쳤을 때 비로소 파일을 받아옵니다. 발표 자료는 몇 MB씩 나가는데 카드만 보고 지나가는 사람에게까지 그걸 받게 할 이유가 없습니다.
  • "미리보기"는 넓은 화면에서만 보입니다. iOS·Android는 페이지 안에서 PDF를 렌더링하지 않아, 모바일에 그 버튼을 두면 눌러도 빈 영역만 남습니다. 대신 "새 탭에서 열기"는 항상 있어서 기기 기본 뷰어로 넘어갑니다.
  • 파일이 없거나 열 수 없으면 용량·페이지 수 없이 카드와 링크만 남습니다. 문서 하나 때문에 글 전체가 죽지 않습니다.

제공되지 않는 것

문서 사이트에서 흔히 보이는 Callout, CodeGroup, Tab, Accordion 같은 컴포넌트는 이 블로그에 없습니다. 글에 그대로 쓰면 다음 오류와 함께 페이지 전체가 실패합니다.

Expected component `Callout` to be defined:
you likely forgot to import, pass, or provide it.

문법을 예시로만 보여주고 싶을 때는 반드시 코드블록으로 감싸야 합니다.

Frontmatter 규칙

문서 최상단의 --- 사이 영역이 메타데이터입니다. 새 글은 이 블록을 채우는 것으로 시작합니다.

---
title: "글 제목"
description: "목록과 검색에 노출되는 한 줄 소개"
pubDate: "2026-07-10"
updatedDate: "2026-08-30" # 선택
category: "Dev"
tags: ["mdx", "blog"]
series: "haengwoon" # 선택 — 같은 값끼리 시리즈로 묶임
status: "public" # 선택 — public | private | hidden (생략하면 public)
---
필드필수설명
title✓글 제목
description✓목록·메타태그에 쓰이는 한 줄 소개
pubDate✓발행일. 시리즈 정렬 기준이라 겹치면 순서가 흔들림
category✓목록 탭 분류
tags✓태그 배열
updatedDate✗최종 수정일
series✗값이 같은 글끼리 연재로 묶임. 비우면 같은 카테고리 글을 추천
status✗공개 범위. 아래 표 참고

공개 범위 (status)

값목록·검색·RSS본문
public노출누구나
private노출 (제목·설명 + 자물쇠 표시)비밀번호 필요
hidden어디에도 안 나옴관리자만
  • 생략하면 public입니다. 기존 글은 전부 이 상태입니다.
  • 오타는 hidden으로 떨어집니다. status: "privat" 같은 실수가 조용히 글을 공개해 버리는 것보다, 안 보여서 바로 알아채는 편이 낫다고 판단했습니다.
  • private 글의 비밀번호는 파일이 아니라 관리자 화면(/admin/posts)에서 설정합니다. 저장소가 공개일 수 있어 해시라도 파일에 두지 않습니다. 비밀번호를 설정하지 않으면 관리자 외에는 아무도 열지 못합니다.

새 글을 올리는 흐름

글은 웹에서 작성하지 않습니다. content/posts/*.mdx 파일이 원본이고, 배포는 git으로 합니다.

# 1. 파일 추가 — 파일명이 곧 URL이 된다
#    content/posts/260710-dev-mdx.mdx  →  /posts/260710-dev-mdx
# 2. 프론트매터 채우고 본문 작성
# 3. 푸시하면 반영
git add content/posts/260710-dev-mdx.mdx
git commit -m "Add MDX guide"
git push
  • 파일명에 공백을 넣지 마세요. URL 인코딩 문제가 생깁니다. 하이픈만 씁니다.
  • status도 파일에 있으므로 공개 범위를 바꾸려면 커밋·배포가 필요합니다. 관리자 화면에서는 현재 상태를 확인만 할 수 있습니다. 대신 오프라인이나 DB 장애 상황에서도 공개 범위가 정확히 지켜집니다.

자주 겪는 파싱 에러

코드블록 안에 코드블록 (백틱 중첩)

가장 함정에 빠지기 쉬운 경우입니다. 백틱 3개짜리 코드블록 안에 다시 백틱 3개를 쓰면, 안쪽 블록이 바깥 블록을 닫아버립니다. 들여쓰기를 넣어도 마찬가지입니다.

Unexpected closing slash `/` in tag, expected an open tag first

해결 — 바깥 블록의 백틱을 4개로 늘립니다.

````mdx        ← 바깥은 백틱 4개
```bash        ← 안쪽은 3개 그대로
echo "hello"
```
````

닫히지 않은 태그

<img src="..."> 처럼 단일 태그를 닫지 않으면 실패합니다. <img /> 형태로 닫습니다.

정의되지 않은 컴포넌트

앞서 다룬 Callout 사례입니다. 대문자로 시작하는 태그는 컴포넌트로 해석되므로, 등록되지 않은 이름을 쓰면 렌더링 시점에 실패합니다.

특수문자 노출

문제 상황해결
본문에 < 를 쓰고 싶다인라인 코드로 감싸거나 &lt; 로 표기
p<0.001 같은 표기숫자 앞의 < 는 태그로 오인됨 → p&lt;0.001
중괄호 {} 를 쓰고 싶다인라인 코드로 감싸거나 \{ 로 이스케이프

댓글