Nextra
Nextra 사이트에 cudoc을 설정합니다. Markdown 문법 확장을 먼저 켜고, 그다음 문서 임베드를 더하며, 마지막의 HTML 내보내기는 선택 사항입니다.
먼저 보기: Nextra 데모는 실행 가능한 예제를 Nextra 4.6.0으로 빌드한 사이트이며, 빌드에 쓴 Markdown 원문으로 가는 링크가 함께 있습니다. → 호스트별 데모
단계는 순서대로 따릅니다. 1–3단계는 Markdown 문법 확장을 켜고, 4–6단계는 한 문서가 다른 문서를 재사용하게 하는 문서 임베드를 더하며, 7단계는 선택 사항입니다.
1단계 — 설치
cudoc, remark 어댑터, Nextra 어댑터를 설치합니다.
npm install @cudoment/cudoc cudoc-remark cudoc-nextra
cudoc이 검증했고 설치한 그대로 빌드되는 Nextra 4.6.0을 유지합니다. npm ls nextra가 4.6.1을 보여 주면 npm install --save-exact nextra@4.6.0 nextra-theme-docs@4.6.0을 실행합니다. 지금 npm이 설치하는 Nextra 4.6.1은 자체 범위로 설치되는 zod 4.4 이상과 함께 쓰면 cudoc 사용 여부와 관계없이 페이지를 하나도 빌드하지 못합니다(shuding/nextra#4989). → 지원 버전
아래 overrides에 "zod": "4.3.6"을 추가해도 4.6.1이 빌드됩니다. 두 방법 모두 GHSA-mrhx-c7xc-m3w5(high, 지나치게 큰 입력이 메모리를 고갈시킴)를 고친 zod 4.6.6보다 오래된 zod를 쓰며, 4.6.1은 zod 4.6.6과 함께 빌드되지 않습니다. npm audit는 이 권고를 4.3.6 고정에서만 보고하고, Nextra 4.6.0이 담은 zod 4.0 베타에서는 보고하지 않습니다.
보안 권고 두 건의 수정판을 overrides로 고정합니다. Next.js 15는 postcss 8.4.31을, Nextra의 수식 지원은 @xmldom/xmldom 0.9.10을 정확한 버전으로 고정하는데, npm audit은 두 패키지 모두에 보안 권고를 보고하지만 이렇게 고정되어 있으면 npm이 수정판을 고를 수 없습니다. 예제 사이트는 수정판을 고정한 상태로 빌드를 확인했으므로, 같은 overrides를 package.json에 기존 항목과 함께 추가하고 다시 설치합니다.
{
"overrides": {
"@xmldom/xmldom": "0.9.12",
"postcss": "8.5.28"
}
}
npm install
Next.js나 Nextra 릴리스가 수정판에 직접 의존하게 되면 해당 항목을 지웁니다. 항목이 아직 필요한지는 npm audit으로 확인할 수 있습니다.
2단계 — remark 플러그인 등록
next.config.mjs에 아래를 병합하고, 사이트의 다른 설정은 그대로 둡니다.
// next.config.mjs
import nextra from "nextra"
import { cudocRemarkPlugins } from "cudoc-nextra"
const withNextra = nextra({
mdxOptions: {
format: "detect",
remarkPlugins: cudocRemarkPlugins({ syntax: {} }),
},
})
export default withNextra({
pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"],
})
기존 Nextra 테마 설정과 MDX 컴포넌트 매핑은 그대로 둡니다. Nextra는 설정된 remark 플러그인을 자체 제목 처리보다 먼저 실행하므로, Nextra가 목차를 만들 때 cudoc의 앵커가 이미 자리를 잡고 있습니다.
사이트는 webpack으로 실행합니다. cudocRemarkPlugins는 Nextra에 플러그인 함수를 넘기는데 Turbopack은 이를 직렬화하지 못하기 때문입니다. Next.js 15는 기본이 webpack입니다. Next.js 16은 따로 지정하지 않으면 Turbopack으로 빌드하므로, 이 단계부터 next dev --webpack과 next build --webpack으로 실행합니다. Next.js 15는 이 옵션을 받지 않습니다.
3단계 — 스타일시트 가져오기
스타일시트는 루트 레이아웃에서 한 번만 가져옵니다.
// app/layout.jsx — add at the top
import "@cudoment/cudoc/styles.css"
cudoc 컴포넌트 매핑은 필요 없습니다.
문법 확장만 필요하면 여기서 멈추어도 됩니다. 사이트를 실행하면 Markdown 문법의 기능들이 동작합니다. 문서 임베드가 필요하면 계속 진행합니다.
4단계 — 임베드 플러그인과 라이브러리 로더 추가
임베드 플러그인은 어댑터가 넣는 플러그인들 뒤에 붙이고, 라이브러리 로더는 같은 파일에 등록합니다. 둘을 모두 넣은 next.config.mjs는 다음과 같으며, 사이트의 다른 설정은 그대로 둡니다.
// next.config.mjs
import nextra from "nextra"
import { cudocRemarkPlugins } from "cudoc-nextra"
import embed from "cudoc-remark/embed"
import { libraryLoader } from "cudoc-remark/loader"
const withNextra = nextra({
mdxOptions: {
format: "detect",
remarkPlugins: [
...cudocRemarkPlugins({ syntax: {} }),
[embed, { sourceRoot: "content", outDir: ".cudoc/documents" }],
],
},
})
export default withNextra({
pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"],
webpack(config) {
config.module.rules.push({
test: /\.mdx?$/,
use: [libraryLoader(".cudoc/documents")],
})
return config
},
})
로더는 번들러가 이미 컴파일한 페이지에 재수집 결과를 전달하는 역할입니다. 이 플러그인은 준비된 내용을 페이지가 컴파일될 때 접합하므로 컴파일된 페이지가 .cudoc/documents/embeds.json에 의존하는데, 로더가 없으면 재수집이 그 페이지에 닿지 않습니다.
webpack 형태만 있습니다. 위와 같이 Nextra에 플러그인 함수를 넘기면 그 로더 옵션을 Turbopack이 직렬화할 수 없으므로, 이렇게 구성한 사이트는 2단계에서 설명한 대로 webpack으로 빌드하고 서비스합니다. Next.js 가이드의 Turbopack 규칙은 여기에 해당하지 않습니다. → 준비된 임베드 접합
5단계 — 수집기 추가
사이트 루트에 collect.mjs를 만듭니다. 네이티브 플러그인 파이프라인과 함께 nextra/compile을 호출해서 결과 문서를 포착하고, 그 결과를 collectDocuments에 넘깁니다. collectDocuments는 라이브러리와 준비된 임베드를 함께 출력하므로, 실행이 실패하면 이전 두 결과가 그대로 남습니다.
// collect.mjs
import { compileMdx } from "nextra/compile"
import { cudocRemarkPlugins } from "cudoc-nextra"
import { createCompilerCapture } from "cudoc-remark"
import { collectDocuments } from "@cudoment/cudoc/node/watch"
// The options next.config.mjs passes to cudocRemarkPlugins.
const documentOptions = { syntax: {} }
await collectDocuments({
sourceRoot: "content",
outDir: ".cudoc/documents",
host: "nextra",
// Change it whenever these settings or the Nextra version change.
compilerId: "nextra-v1",
...documentOptions,
async compiler(source, { filePath, options }) {
const capture = createCompilerCapture()
await compileMdx(source, {
filePath,
codeHighlight: false,
mdxOptions: {
format: options.format,
remarkPlugins: [...cudocRemarkPlugins(documentOptions), capture.remark],
rehypePlugins: [capture.rehype],
},
})
return capture.read()
},
})
수집과 렌더링은 반드시 일치해야 합니다. sourceRoot, 문법 설정, 네이티브 컴파일러 옵션, 경로를 콘텐츠 설정에 맞추고, 관련 설정이나 의존성 버전이 바뀌면 compilerId를 바꿉니다.
수집기는 문서 옵션을 documentOptions 객체 하나에 담아 collectDocuments 설정에 펼쳐 넣고, 컴파일러 안의 cudocRemarkPlugins 호출에도 넘깁니다. 이 객체에는 next.config.mjs가 cudocRemarkPlugins에 넘기는 옵션을 그대로 적고, 한쪽을 바꾸면 다른 쪽도 바꿔야 합니다.
6단계 — 빌드 전마다 수집 실행
package.json에 스크립트를 추가합니다.
{
"scripts": {
"collect": "node collect.mjs",
"check": "cudoc check --config cudoc.config.mjs",
"dev": "npm run collect && next dev",
"build": "npm run collect && npm run check && next build"
}
}
위 명령은 Next.js 15 기준입니다. Next.js 16에서는 2단계에서 설명한 대로 next dev --webpack과 next build --webpack으로 씁니다.
cudoc check는 collect 다음에 실행합니다. 수집기가 게시한 라이브러리를 읽기만 하고 수집은 하지 않으므로, 설정 파일에는 그 라이브러리의 위치와 사이트가 루트 기준 이미지를 제공하는 디렉터리만 적으면 됩니다.
// cudoc.config.mjs
export default {
sourceRoot: "content",
outDir: ".cudoc/documents",
check: { assetDirs: ["public"] },
}
수집은 Nextra보다 먼저 돌아야 합니다. 글을 쓰는 동안 다시 수집하려면 collectDocuments 대신 @cudoment/cudoc/node/watch의 watchDocuments에 같은 옵션을 넘깁니다. 로더 규칙이 회차마다의 결과를 개발 서버가 이미 컴파일한 페이지에도 전달합니다.
cudoc check는 깨진 링크, 앵커, 이미지, 임베드를 한 번에 전부 보고하고 종료 코드를 0이 아닌 값으로 냅니다. 사이트가 생성되기 전에 빌드가 멈춥니다. → 참조 검사
7단계 — HTML로도 내보내기 (선택)
cudoc-export를 설치하고 수집된 라이브러리를 HTML로 내보냅니다.
npm install cudoc-export
npx cudoc-export build content --library .cudoc/documents --out-dir shared-html \
--links host --host-url https://docs.example.com/project/ --asset-dir public
소스 디렉터리는 docs가 아니라 content입니다. Nextra 빌드 결과와 수집 데이터는 변경되지 않습니다. → 내보내기
이제 쓸 수 있는 것
아래 표에서 문법 행은 3단계 뒤에, 임베드 행은 6단계 뒤에 동작합니다.
| 기능 | 예시 | 자세히 |
|---|---|---|
| 명시적 제목 앵커 | ## 한도 (#limits) |
문법 |
| 제목 배지 | ## 한도 (#limits) (@New) |
문법 |
| 제목이 있는 콜아웃 | > [!NOTE] 시작하기 전에 |
문법 |
| 표 셀 안 중첩 목록 | - 계정<br />-- 이메일 인증 |
문법 |
| 표 셀 병합 | 셀에 ^^나 >>만 쓰기 |
문법 |
| 코드 제목과 강조 줄 | ```js title="retry.js" {2} |
문법 |
| 탭 | > [!TAB] npm |
문법 |
| 변수 | (=version) |
문법 |
| 문서 전체 임베드 | sources: [reference.md] |
임베드 |
| 한 절 임베드 | sources: [reference.md#limits] |
임베드 |
| 제목 요약 표 | select: { depth: 2 } |
임베드 |
| 문서 트리 | render: { type: tree } |
임베드 |
| 사본의 문구 치환 | replace: [{ find, replace }] |
임베드 |
Nextra에서 알아 둘 점
네이티브 문법과 함께 쓰기. Nextra의 [#id] 앵커와 정적 <Callout> 컴포넌트는 그대로 동작합니다. 임베드한 사본과 HTML 내보내기가 같은 의미를 갖도록 cudoc이 그것들까지 정규화하게 하려면 다음과 같이 설정합니다.
cudocRemarkPlugins({ syntax: { headingAnchor: "both", callout: "both" } })
both를 켤 때는 수집기의 documentOptions도 함께 바꿔야 합니다. 지원하는 네이티브 형태는 문법 가이드에 정리되어 있습니다.
목차는 Nextra가 계속 담당합니다. 어댑터는 host: "nextra"를 설정하고, 정규화가 명시적 id를 각 제목에 붙이며, 목차는 Nextra에 맡깁니다. Nextra는 완성된 페이지로 목차를 만들므로 임베드로 들어온 제목도 목차에 들어갑니다.
제목 id는 slug로 바뀝니다. Nextra는 자체 [#id]를 포함한 모든 제목 id를 slugger에 통과시키므로, id는 이미 slug 형태일 때만 철자가 유지됩니다. ## 버전 (#v1.2)은 v12로 렌더링됩니다. 앵커는 영문 소문자, 숫자, 하이픈으로 씁니다.
이 slugger는 id를 문서 순서대로 받습니다. 따라서 앞선 제목의 글자가 이미 만든 id를 뒤에서 명시하면(## Setup 다음 ## Intro (#setup)) 여기와 Docusaurus에서는 명시한 쪽이 setup-1이 됩니다. VitePress, Eleventy, 독립 내보내기는 명시한 id에 setup을 남기고 앞의 제목에 번호를 붙입니다. 명시한 id를 가진 제목을 같은 id를 만들 글자의 제목보다 앞에 두면, slug 형태의 앵커는 모든 호스트에서 같게 해석됩니다.
정적 컴포넌트는 문서 노드가 되고 동적 컴포넌트는 컴포넌트로 남습니다. 정적 <Callout>은 이식 가능한 콜아웃으로 정규화되어 모든 호스트와 HTML 내보내기가 렌더링할 수 있습니다. 임베드된 절에 남은 컴포넌트는 복사본이 접합되는 자리에서 Nextra의 컴포넌트 매핑으로 렌더링되며, HTML 내보내기에는 여전히 렌더러가 필요하고 cudoc check가 그 이름을 알려 줍니다.
.md와 .mdx의 차이. 직접 만든 React 컴포넌트는 .mdx에 작성합니다. .md는 Markdown으로 남아 {value}가 그대로 글자가 됩니다. → .md와 .mdx 선택
다음으로
이어서 읽을 문서는 임베드 가이드, 어댑터 내부 동작, 실행 가능한 예제입니다.