cudoc

문서 임베드

English | 한국어 · 전체 가이드

임베드는 수집한 문서나 절을 다른 페이지에서 재사용합니다. 이 가이드는 수집, 선택, 요약 표, 트리, 찾기·바꾸기, 갱신을 다룹니다.

작성자는 cudoc-embed 코드 블록을 쓰고, 호스트 연동이 그 결과를 삽입합니다. cudoc은 이 블록을 참조로 남기지 않고 빌드 시점에 실제 문서 노드로 해석하며, 공용 조각 파일을 임포트하는 방식과 갈리는 지점이 바로 여기입니다. → 공용 컴포넌트로 만들면 안 되나요?

문서 수집 설정

수집은 문서 간 임베드가 읽는 저장된 AST와 원문 스냅샷을 만듭니다. 문법 확장만 쓸 때는 수집이 필요 없습니다. 일반 Markdown 또는 표준 cudoc 파이프라인을 사용하는 Next.js에서는 cudoc.config.mjs를 작성합니다.

export default {
  sourceRoot: "docs",
  outDir: ".cudoc/documents",
  host: "markdown", // Next.js 가이드에서는 "next"를 사용합니다.
  syntax: {},
}

그다음 수집합니다. 이 명령이 임베드도 함께 준비합니다.

npx cudoc collect --config cudoc.config.mjs

같은 입력 디렉터리와 문법 설정으로 호스트를 빌드하거나 실행하되, 수집 결과를 읽도록 설정합니다. 방법은 Next.js, Docusaurus, Nextra, VitePress, Eleventy 가이드에 있습니다. HTML 내보내기는 빌드 명령 안에서 이 과정을 처리합니다.

각각 다른 경로로 서비스되는 여러 디렉터리의 문서는 sourceRoot 대신 roots로 수집합니다. exclude는 **/AGENTS.md 같은 파일을 제외하고, private는 수집과 검사는 하되 내보내지는 않는 문서를 표시합니다. 옵션과 ID 파생 규칙은 수집에서 설명합니다.

export default {
  roots: [
    { dir: "content", base: "docs" }, // content/ko/guide.md → /docs/ko/guide
    { dir: "glossary", base: "terms" },
  ],
  exclude: ["**/AGENTS.md", "docs/ko/drafts/**"],
  private: ["docs/in/**"],
  outDir: ".cudoc/documents",
  host: "markdown",
}

Docusaurus, Nextra, VitePress, Eleventy는 호스트에 설정된 실제 컴파일러로 수집합니다. 범용 파서로 다시 읽는 것만으로는 호스트 고유 변환을 재현할 수 없기 때문이며, 각 가이드가 그 호스트의 수집기를 보여 줍니다. Next.js에 별도 플러그인을 추가했다면 수집기에도 같은 컴파일러 구성을 씁니다. 사용자 컴파일러에는 compilerId가 필요하며, 컴파일러 버전이나 관련 설정이 바뀌면 갱신합니다.

섹션 가져오기

docs/index.md에서 블록 하나가 복사할 섹션을 지정합니다.

```cudoc-embed
sources: [reference.md#limits]
```

이 블록은 제한 섹션의 제목, 본문, 하위 섹션을 가져옵니다. 섹션은 같은 단계 또는 상위 단계의 다음 제목 직전까지입니다. → 왜 컴포넌트가 아니라 코드 블록인가

#limits를 생략하면 문서 전체를 가져오며, 확장자는 생략할 수 있습니다. 경로는 임베드를 작성한 문서 기준이고, /로 시작하면 운영체제 루트가 아닌 sourceRoot 기준이며, 원본은 수집된 로컬 문서여야 합니다.

하위 섹션을 빼려면 includeChildren: false로 섹션을 선택합니다.

```cudoc-embed
sources: [reference.md]
select:
  anchors: [limits]
  includeChildren: false
render: section
```

select는 anchors, 정확히 일치하는 titles, 1–6의 숫자 또는 배열인 depth를 받으며, 섹션은 적은 조건을 모두 만족해야 합니다. 제목이 바뀔 수 있으면 앵커를 쓰고, 자식 내용을 반복할 뜻이 아니면 부모·자식 섹션을 함께 선택하지 않습니다.

제목 요약 표 만들기

표 임베드는 선택한 제목마다 행 하나를 만듭니다. 다음은 두 문서의 ## 절을 나열합니다.

```cudoc-embed
sources: [reference.md, advanced.md]
select:
  depth: 2
render:
  type: table
  columns: [title, link, summary]
```

title은 표시 제목, link는 원본 섹션 링크, summary는 섹션의 첫 문단을 일반 텍스트로 만든 값입니다. 필요한 열과 순서를 고를 수 있으며 기본 순서는 title, link, summary입니다.

열을 직접 정의하기

열을 매핑으로 쓰면 그 열의 글자가 어디서 오는지, 무엇으로 연결되는지, 어느 폭을 유지해야 하는지를 적을 수 있습니다. 다음은 API마다 하나씩 있는 "기본 정보" 절을 개요 표의 행 하나로 바꿉니다.

```cudoc-embed
sources: [/docs/rest-api.md]
select:
  depth: 5
  titles: [기본 정보]
render:
  type: table
  columns:
    - { header: API, value: parent, link: parent, minWidth: 10rem }
    - header: 메서드
      value: { row: 1, column: 0, skipTablesWithHeaders: [요구 사항] }
    - header: URL
      value: { row: 1, column: 1, skipTablesWithHeaders: [요구 사항] }
    - { header: 설명, value: summary }
    - { header: 레퍼런스, value: { extractor: sdkReference } }
```
키 의미
header 머리 셀의 글자. 기본값은 값 이름이며, 셀 좌표나 { extractor } 값이면 빈 셀입니다.
value: title 행이 된 절의 제목 글자.
value: summary 절의 첫 문단을 일반 텍스트로 만든 값.
value: parent 그 절보다 얕은 가장 가까운 상위 제목의 글자. "기본 정보" 위에 있는 API 이름이 여기에 해당합니다.
value: { row, column, table?, skipTablesWithHeaders? } 절 안의 표 하나에서 셀 하나. 머리 행을 0으로 하여 0부터 셉니다. table은 절의 표를 순서대로 고르되, 머리 행에 skipTablesWithHeaders의 이름이 있는 표는 세지 않습니다. 셀을 합친 표에서는 각 행이 아직 가진 셀만 셉니다.
value: { extractor } 수집 옵션에 그 이름으로 등록한 함수. 아래에서 설명합니다.
link section은 행의 절로, parent는 상위 제목으로, document는 문서로 연결합니다. 생략하면 글자만 넣습니다.
minWidth 120px, 10rem 같은 CSS 길이이며 머리 셀의 min-width 스타일로 쓰입니다. HTML 사이트는 스타일로 반영하고 Word 내보내기는 그 열을 최소 그 폭으로 유지합니다.

축약형 열은 각각 { value: title }, { value: title, link: section }, { value: summary }와 같고 머리 글자는 그 이름입니다.

고정 어휘로 부족한 것은 추출기가 맡습니다. 수집 옵션에 함수를 등록하고 열에서 그 이름을 부릅니다. 함수는 행(원본 문서, 선택된 절, 상위 제목, 절의 주소)을 받아 글자 또는 글자와 링크를 돌려줍니다.

// cudoc.config.mjs
export default {
  sourceRoot: "docs",
  extractors: {
    sdkReference: {
      version: "2026-09-21", // 함수의 출력이 바뀌면 함께 바꿉니다
      extract(row) {
        const language = row.document.id.split("/")[1]
        return {
          text: `${row.section.title} (${language})`,
          url: `/sdk/${language}/${row.section.anchorId}`,
        }
      },
    },
  },
}

version은 라이브러리 설정에 들어가므로, 추출기가 바뀌면 컴파일러가 바뀔 때처럼 준비된 임베드가 무효가 됩니다. 아무것도 찾지 못한 열은 빈 셀로 렌더링되며, 매핑으로 쓴 열에 대해서는 cudoc check가 그런 셀마다 열이 요구한 것과 절이 가진 것을 함께 적은 empty-embed-cell 경고를 보고합니다. → 참조 검사

문서 트리 그리기

트리는 폴더가 문서를 품은 모양 그대로 문서를 한 줄씩 나열하고, open 아래의 단계는 독자가 펼치기 전까지 접어 둡니다. 다른 임베드와 같은 수집만 필요하므로, 블록을 쓰고, 수집하고, 빌드하면 됩니다. 트리의 모양은 호스트 가이드마다 가져오는 @cudoment/cudoc/styles.css가 정합니다. 이 스타일시트가 없으면 호스트가 접힌 단계를 직접 쓴 details처럼 그리며, Docusaurus와 Nextra에서는 상자 모양이 됩니다.

첫 트리 만들기

파일마다 # 제목과 한 문장 요약이 있는 가이드 묶음을 예로 듭니다.

docs/
├── index.md
├── reference.md          # API reference, ##와 ### 절이 있음
└── guides/
    └── payments/
        ├── index.md          # Payments: 트리를 넣을 페이지
        ├── overview.md       # Overview
        ├── checkout.md       # Checkout
        ├── checkout/
        │   ├── cards.md      # Cards
        │   └── wallets.md    # Wallets
        └── refunds/
            ├── index.md      # Refunds
            └── partial.md    # Partial refunds

docs/guides/payments/index.md에 다음과 같이 씁니다.

```cudoc-embed
sources: [./]
render:
  type: tree
  order: [Overview, ...]
```

그러면 페이지에 다음 트리가 보이며, 제목마다 그 문서로 연결됩니다. ▾는 처음부터 펼쳐 있는 줄, ▸는 처음에 접혀 있는 줄, •는 아래에 줄이 없는 줄입니다.

• Overview · Where to start.
▾ Checkout · Take a payment.
    • Cards · Card payments.
    • Wallets · Wallet payments.
▾ Refunds · Give money back.
    • Partial refunds · Refund part of a payment.

./는 페이지가 있는 폴더이고, 페이지 자신인 index.md는 줄이 되지 않고 그 폴더를 대표합니다. checkout.md는 옆의 checkout/ 폴더를 하위로 받고, refunds/index.md는 자기 폴더를 대표합니다. checkout.md처럼 옆의 폴더를 대표하는 페이지는 그 폴더를 sources: [checkout/]로 직접 적습니다. 이 페이지의 ./는 페이지가 놓인 폴더라서 Checkout 자신까지 형제 문서와 함께 나열하기 때문입니다.

order가 Overview를 맨 앞에 두고 나머지는 제목 순으로 뒤따릅니다. 아래에 다른 줄이 있는 줄은 그 줄들을 HTML details 요소로 접기 때문에, 모든 호스트와 내보낸 HTML에서 스크립트 없이 트리를 열고 닫을 수 있습니다. 인쇄 HTML, PDF, Word에는 접을 곳이 없으므로 트리를 펼친 중첩 목록으로 씁니다.

자주 쓰는 트리 네 가지

문서 묶음의 개요 페이지는 위의 트리입니다. 폴더의 index.md에 sources: [./]를 씁니다.

긴 레퍼런스의 접이식 개요는 문서 아래에 그 문서의 절을 나열합니다. reference.md 옆의 docs/index.md에 쓰며, headings: 2가 ##와 ### 제목을 가져오고, open: 2가 두 단계를 처음부터 펼칩니다.

```cudoc-embed
sources: [reference.md]
render:
  type: tree
  headings: 2
  open: 2
```
▾ API reference · Every endpoint, its limits and how to call it.
    ▾ Limits · How many calls you may make.
        • Rate limits · Calls per minute.
        • Quotas · Calls per day.
    ▾ Authentication · How to sign a call.
        • Tokens · Where a token comes from.

절 하나의 개요는 앵커에서 시작합니다. headings는 절 자신의 단계와 상관없이 ##부터 세므로, ## 절의 ### 제목을 가져오려면 headings: 2를 씁니다.

```cudoc-embed
sources: [reference.md#limits]
render:
  type: tree
  headings: 2
```
▾ Limits · How many calls you may make.
    • Rate limits · Calls per minute.
    • Quotas · Calls per day.

전체 문서 지도는 라이브러리 맨 위에서 시작합니다. depth: 2는 두 단계만 담고, open: 0은 그 단계를 접어 두며, columns: [link]는 제목만 보여 줍니다.

```cudoc-embed
sources: [/]
render:
  type: tree
  depth: 2
  open: 0
  columns: [link]
```
• API reference
▸ Payments

Payments를 펼치면 Checkout, Overview, Refunds가 나옵니다. Cards, Wallets, Partial refunds는 세 번째 단계라서 depth 밖에 있습니다. guides/를 대표하는 문서가 없으므로 이 폴더는 단계를 더하지 않습니다.

어느 문서가 어디에 달리는가

문서와 절이 트리의 어디에 달리는지는 다섯 규칙이 정합니다.

설정

render는 type: tree 옆에 다음 키를 받습니다.

키 의미 기본값
open 웹에서 처음부터 펼쳐 둘 단계 수입니다. 0이면 첫 층만, 1이면 첫 두 단계가 보입니다 1
print 인쇄 HTML, PDF, Word가 보여 줄 단계 수입니다 모든 단계
depth 트리에 담을 단계 수이며, 제목 줄도 문서 줄과 함께 셉니다 모든 단계
headings 각 문서의 제목 아래에서 줄이 될 제목 단계입니다. 1은 ##, 2는 ##와 ###이며 5까지 쓸 수 있습니다 0, 문서만
order 첫 층에서 먼저 둘 이름을 순서대로 적습니다. ...는 적지 않은 나머지 이름 전체의 자리이며, 없으면 나머지가 뒤에 옵니다 제목 순
columns 줄 하나에 보일 값입니다. 추출기를 포함한 표 열의 어휘를 쓰며, 비어 있지 않은 값을 ·로 잇습니다 [link, summary]

줄에 보이는 것

문서의 제목은 # 제목이며, 없으면 title 앞부분 정보, 그것도 없으면 파일 이름이고, index.md라면 폴더 이름입니다. 절의 제목은 그 제목 줄의 글자입니다.

summary는 요약 표와 같은 방식으로 제목 다음의 첫 문단을 읽습니다. 아웃라이너가 # 제목 위에 쓰는 속성 줄은 요약이 되지 않습니다. 제목 아래 다음 제목 전까지 자기 문단이 없으면 제목 다음의 첫 문단이 하위 절의 문단이 되므로, 그 줄은 아래 줄의 요약을 되풀이합니다. 절에 첫 문장을 두거나, columns: [link]로 제목만 보이게 하면 됩니다.

columns는 표의 어휘를 씁니다. [link]는 제목만, [title, summary]는 연결하지 않은 제목을 보여 주고, { value: { row, column } }은 절 안의 표에서 값을 하나 가져오며, 그 밖의 값은 추출기로 계산합니다. 값이 빈 열은 줄에서 빠지며, 모든 열이 비어 있는 줄에는 제목이 보입니다.

트리는 절의 본문을 복사하지 않으므로 select와 replace를 쓸 수 없습니다.

줄의 순서

첫 층은 sources에 적은 순서를 따르며, 폴더 원본이 가져온 문서는 그 안에서 정렬합니다. 그 아래 단계는 모두 정렬하되, 절은 문서 안의 순서를 유지합니다. 문서는 제목 순으로 정렬하며, 대소문자를 무시하고 숫자는 값으로 읽어서 어느 컴퓨터에서나 같은 결과가 나옵니다.

그다음 order가 첫 층의 위치를 옮깁니다. 항목은 확장자를 뺀 문서 파일 이름, index.md라면 폴더 이름, 또는 줄의 제목과 일치해야 하며, 정렬과 달리 대소문자까지 정확히 같아야 합니다. 이름은 유니코드 NFC로 비교하므로, macOS의 Syncthing처럼 동기화 도구가 NFD로 저장한 파일 이름도 직접 입력한 조합형 이름과 일치합니다. ...는 어느 목록 표기에서든 따옴표가 필요 없지만, : 나 #가 든 이름은 YAML이 매핑과 주석으로 읽으므로 따옴표로 감쌉니다. 대괄호로 쓴 목록에서는 쉼표나 [, ], {, }가 든 이름도 따옴표로 감쌉니다.

order: ["Step 1: Install", "Install, configure and run", ..., Changelog]

비공개 문서

private가 아닌 페이지의 트리는 폴더나 상위 문서로 들어올 private 문서를 그 아래 줄과 함께 빼는데, 내보내기가 비공개 문서를 쓰지 않아 그 문서로 링크할 수 없기 때문입니다. 비공개 페이지는 이런 문서도 나열합니다. 원본에 직접 적은 비공개 문서는 그대로 나열하며, 그 줄의 링크는 직접 쓴 링크와 같은 정책을 따릅니다. 내보내기는 relative에서는 그 링크를 거부하고 host에서는 배포된 페이지를 가리킵니다. → 하이퍼링크 동작 선택

공개 페이지에서 문서가 모두 빠지는 폴더는 오류이고, 빠진 문서를 가리키는 order 항목은 아무것과도 일치하지 않습니다. exclude로 뺀 파일은 라이브러리에 없으므로 어느 트리에도 나오지 않습니다.

하위 단계의 합계

트리에서 추출기는 가장 깊은 줄부터 줄마다 실행되며, 실행 문맥에는 아래 줄이 모두 채워진 그 줄이 node로 들어 있습니다. 다음은 문서와 그 아래 문서 전체의 남은 할 일을 셉니다.

// cudoc.config.mjs
const open = (text) => (text.match(/^- TODO /gm) ?? []).length

export default {
  sourceRoot: "notes",
  extractors: {
    openTasks: {
      version: "1",
      extract(row, { library, node }) {
        const total = (line) =>
          open(
            library.documents.find((d) => d.id === line.documentId).source.text,
          ) + line.children.reduce((sum, child) => sum + total(child), 0)
        const count = node ? total(node) : open(row.document.source.text)
        return count ? `${count} open` : undefined
      },
    },
  },
}
```cudoc-embed
sources: [/projects/]
render:
  type: tree
  columns: [link, { value: { extractor: openTasks } }, summary]
```

depth 아래의 줄은 트리에 없으므로 합계도 depth에서 멈춥니다. 줄의 추출기가 실행될 때에는 아래 줄의 칸이 이미 채워져 있으므로, 하위 줄이 보여 주는 값을 더하는 방식으로도 합계를 구할 수 있습니다.

트리 검사와 갱신

cudoc check는 아무것도 가리키지 않는 원본과 앵커를 다른 임베드와 같이 보고하고, 설정에 등록되지 않은 추출기를 invalid-embed-spec으로, 첫 층의 어느 줄과도 일치하지 않는 order 항목을 unmatched-tree-order 경고로 보고합니다. → 참조 검사

준비된 트리는 줄을 읽어 온 모든 문서에 의존하므로, 아래 문서를 고치면 다음 수집(cudoc collect --watch의 다음 회차 포함)에서 트리를 다시 해석합니다. 문서를 더하거나 지우면 모든 임베드를 다시 해석하므로, 새 파일은 다음 회차에 자기가 속한 트리에 나타납니다.

아웃라이너의 블록처럼 트리를 자기 형식으로 쓰는 프로그램은 resolveTree로 같은 줄을 읽습니다.

찾기·바꾸기

replace는 원문에 적용하는 규칙으로 복사본의 표현을 바꿉니다. 다음 블록은 일반 문자열 하나와 패턴 하나를 바꿉니다.

```cudoc-embed
sources: [reference.md#limits]
replace:
  - find: "**기본값**"
    replace: "_적용값_"
  - find: "v[0-9]+"
    replace: "current"
    regex: true
    flags: g
```

규칙은 선택한 원본 Markdown 소스에 순서대로 적용되며, 따로 지정하지 않으면 하위 절도 포함합니다. 일반 문자열 규칙은 일치하는 모든 부분을 바꾸고 대소문자를 구분합니다. 정규식 규칙은 regex: true로 켜며 기본 플래그는 g입니다. cudoc은 Markdown 표기를 유지한 채 결과를 원본 문서의 컴파일러 설정으로 다시 컴파일하고, 원본 파일과 수집된 AST는 그대로 둡니다.

선택한 내용이 참조하는 외부 참조 정의는 계속 쓸 수 있습니다. 규칙은 선택한 원문 범위만 바꾸며, 그 범위 밖에서 덧붙는 정의는 바꾸지 않습니다.

어디에 적용되는가

치환은 트리에서 무언가를 골라내기 전에 원문 슬라이스 단계에서 일어나므로 절 글을 복사하는 모든 임베드 형태에 적용됩니다. 하위 절을 포함하든 빼든, 문서 전체를 가져오든, 원본을 여러 개 쓰든, 제목이나 깊이로 고르든 마찬가지입니다. render: { type: table } 요약 표에서는 제목 열과 요약 열이 바뀌고, 링크 열은 원본 문서의 실제 앵커를 그대로 가리킵니다. 트리는 절 글을 복사하지 않으므로 replace를 거부합니다. → 줄에 보이는 것

닿지 않는 곳이 두 군데 있습니다. 가져오는 절 안에 있는 cudoc-embed 블록은 자기 규칙대로 따로 해석되므로, 바깥 규칙은 그 중첩 임베드가 끌어온 내용을 바꾸지 않습니다. 그리고 규칙 목록은 선택된 절 전부에 적용되므로, depth: 2로 여러 절을 고른 상태에서 한 절만 겨냥한 규칙은 나머지 절에서 아무것도 찾지 못하며, 이것은 오류가 아닌 정상 동작입니다.

제목이 다른 블록 안에 있는 절은 그 원문을 따로 읽었을 때의 글자를 치환합니다. 목록 항목 안의 절은 아무리 깊어도, 파서가 항목 내용에서 뗀 것처럼 항목의 표시와 들여쓰기를 그 글자에서 떼어 내므로 평소처럼 치환합니다. 최상위 절 안에 인용문이나 목록이 들어 있는 경우에도 평소처럼 치환합니다.

바깥 블록의 일부가 원문에 남는 경우에는 규칙이 일치하든 안 하든 모두 거부합니다. 제목 줄의 제목 앞에 인용문이나 콜아웃의 >가 있을 때, 제목이 각주 안에 있을 때, 절이 컴포넌트나 ::: 컨테이너의 마지막 절이라 블록을 닫는 줄까지 원문에 들어갈 때입니다.

이때 빌드는 cudoc: <document>#<id> starts inside a quote, and the `>` marks stay on the lines after its heading, so a replace rule cannot rewrite it; … 오류로 실패합니다. cudoc check는 unreplaceable-embed-section으로 보고하며, 거부되는 절을 select가 함께 고르면 그 임베드 전체가 멈춥니다. 제목을 그 블록 밖으로 옮기거나, 블록이 닫히기 전에 다른 제목으로 절을 끝내거나, replace 없이 임베드해야 합니다. replace가 없으면 수집한 그대로 복사합니다.

find를 안전하게 쓰는 법

목록 항목 안의 절에서는 find가 항목의 표시와 들여쓰기를 뗀 글자, 즉 절을 따로 읽었을 때의 글자를 봅니다. 규칙도 그 표시와 들여쓰기를 뺀 줄을 기준으로 씁니다. Alpha가 아니라 Alpha입니다.

find는 따옴표로 감쌉니다. YAML 스칼라이므로, 따옴표 없는 글자는 아래처럼 조용히 틀리거나 파싱이 깨집니다.

이렇게 쓰면 이렇게 됩니다
find: "**굵게**" 정상
find: **굵게** YAML 오류. *가 별칭 표기로 해석됨
find: 한도 # 분당 조용히 한도로 잘림. #부터 주석
find: 참고: 이것 YAML 오류. 콜론이 하나 더
find: @New YAML 오류. @는 예약 문자

정규식은 한 단계가 더 필요합니다. 큰따옴표 안에서는 \가 이스케이프 문자이기 때문입니다.

이렇게 쓰면 실제로 전달되는 패턴
find: "\d+" YAML 오류. 잘못된 이스케이프
find: "\\d+" \d+
find: '\d+' \d+. 작은따옴표는 글자 그대로 받음

역슬래시가 들어가면 작은따옴표가 편합니다. 여러 줄을 찾을 때는 블록 스칼라를 씁니다.

- find: |-
    첫째 줄
    둘째 줄
  replace: "한 줄로"

찾는 대상이 없을 때

아무것도 찾지 못한 규칙은 아무것도 실패시키지 않습니다. 임베드는 다르게 쓰일 것을 전제로 작성된 자리에 원본의 표현을 그대로 싣고, 빌드는 아무 말도 하지 않습니다. cudoc check가 이것을 보고합니다.

  6:12  warning unmatched-embed-replacement was reworded away
        replacement 1 found no "was reworded away" in what this embed copies
        from reference, so nothing was changed. …

선택된 절 어디에서도 맞지 않은 규칙만 보고하므로, 위의 절별 부분 불일치는 조용히 넘어갑니다.

왜 컴포넌트가 아니라 코드 블록인가

임베드는 <Embed sources={...} /> 같은 컴포넌트가 아니라 펜스 코드 블록입니다. 컴포넌트 형태를 막는 조건이 세 가지 있고, 그와 별개로 코드 블록이 더 나은 이유가 하나 더 있기 때문입니다.

.md에는 컴포넌트가 없습니다. MDX는 .md에서 JSX를 끄기 때문에, 거기 쓴 <Embed />는 요소가 아니라 문단 안의 raw HTML로 파싱됩니다. 임베드를 컴포넌트로 만들면 이 기능을 쓰는 문서를 전부 .mdx로 바꿔야 하며, 이것은 이 프로젝트가 어디서나 지키는 원칙, 즉 작성자는 Markdown만 쓰고 아무것도 등록하지 않는다는 원칙과 어긋납니다.

여섯 호스트 중 둘에는 MDX 자체가 없습니다. VitePress와 Eleventy는 markdown-it 위에 있어서 JSX를 파싱하지 못하고, .mdx 파일은 수집 단계에서 거부됩니다. 컴포넌트 문법이었다면 이 두 호스트를 빼거나 호스트마다 다른 표기를 만들어야 했을 텐데, 그것은 공용 조각 파일을 옮기기 어렵게 만드는 바로 그 분화입니다. 코드 블록은 어디서나 code 노드 하나이고, 파이프라인 전체가 술어 하나로 찾습니다.

node.type === "code" && node.lang === "cudoc-embed"

스펙은 렌더링되기 전에 읽힙니다. prepareEmbeds는 React가 실행되기 한참 전인 빌드 시점에 저장된 AST를 훑습니다. JSX props로 쓴 replace={[{ find: "a", replace: "b" }]}는 속성 표현식이고 그 파싱 결과는 estree에 담기는데, 이식 가능한 문서 AST가 JavaScript 구문 트리를 들고 다닐 이유가 없으므로 수집은 estree를 제거합니다. 남는 것은 [{ find: "a", replace: "b" }]라는 소스 문자열뿐이라, 스펙을 되읽으려면 문서 안의 JavaScript를 다시 파싱하거나 평가해야 합니다. 코드 블록의 값은 처음부터 문자열이고 YAML이 그대로 읽습니다.

다른 곳에서 얌전히 물러납니다. cudoc을 모르는 파서는 이것을 code 노드로 보고 회색 블록으로 렌더링하며, 다시 Markdown으로 써도 한 글자도 바뀌지 않습니다. GitHub에서도 그대로 보입니다. 같은 자리에 컴포넌트 형태가 있었다면 깨진 마크업이거나 글자 그대로 노출되는데, 문서가 여기저기로 옮겨 다닌다는 것이 이 기능의 전제입니다. 원문 치환도 펜스가 원본 Markdown에서 잘려 나갔다가 다시 컴파일되는 왕복을 그대로 통과한다는 같은 성질에 기댑니다.

빌드와 갱신

빌드는 네 단계로 진행하며, 네 번째 단계가 앞의 두 단계를 언제 다시 실행하는지 정합니다.

  1. 전체 원본 문서를 수집합니다.
  2. 임베드를 준비합니다. cudoc collect에는 이 단계가 포함되어 있으며 라이브러리와 임베드를 함께 출력하므로, 실패한 수집은 이전 출력을 그대로 남깁니다.
  3. 호스트를 빌드하거나 개발 서버를 실행합니다.
  4. 원본 문서, 문법, 경로, 컴파일러 설정이 바뀌면 수집과 준비를 다시 실행합니다. 실행 중인 호스트가 라이브러리를 메모리에 보관한다면 재시작합니다.

MDX 호스트에서는 임베드 플러그인과 같은 파일에 cudoc-remark/loader를 등록합니다. 임베드 플러그인은 페이지가 컴파일될 때 준비된 내용을 접합하고 번들러는 그 페이지의 파일이 바뀌기 전까지 컴파일된 페이지를 그대로 제공하므로, 로더가 있어야 재수집이 개발 서버와 영구 빌드 캐시가 이미 컴파일한 페이지에도 닿습니다. 등록 위치는 Next.js, Nextra, Docusaurus 가이드에 있습니다.

로더는 파일을 그대로 두고, 컴파일 입력에 아무것도 렌더링하지 않는 참조 정의 한 줄만 더합니다.

패키지 스크립트의 dev와 build 앞에 수집을 연결해 빌드가 오래된 라이브러리로 돌아가지 않게 하고, 생성된 .cudoc/은 버전 관리에서 제외하고 CI에서 다시 생성합니다.

글을 쓰는 동안에는 개발 서버 옆에서 cudoc collect --watch --config cudoc.config.mjs를 실행하면 루트 아래의 문서가 바뀔 때마다 다시 수집합니다. 원문이 바뀐 문서만 컴파일하고 바뀐 문서를 읽는 임베드만 다시 해석하며, 컴파일되지 않는 문서나 해석되지 않는 임베드는 메시지로 남고 마지막으로 성공한 라이브러리와 그 임베드가 그대로 유지됩니다. 호스트 전용 수집기는 범용 명령 대신 node collect.mjs로 실행하며, 같은 반복을 watchDocuments로 씁니다.

경로 설정은 요약 표와 임베드 안의 문서 링크를 실제 페이지에 맞춥니다. routeBase에는 /docs 같은 문서 경로 접두사를 지정하며, VitePress의 cleanUrls: false에는 routeSuffix: ".html"이, Eleventy의 기본 디렉터리 URL 규칙에는 routeSuffix: "/"가 필요합니다. 호스트의 사용자 경로와 frontmatter slug는 자동 추론하지 않으므로 routes: { "guide/start": "/custom/start" }로 지정합니다.

문제 해결

다음 표는 증상마다 해결하는 조치를 짝짓습니다.

증상 조치
준비된 임베드가 없거나 오래됨 호스트 실행 전에 다시 수집·준비하고 양쪽 출력 경로가 같은지 확인
문서 또는 섹션을 찾을 수 없음 상대 경로, 수집 범위, 명시적 앵커 ID 확인
ID 중복 또는 충돌 제목에 고유 ID 지정; API로 여러 임베드를 조합하면 서로 다른 prefix 사용
순환 임베드 메시지가 알려 주는 순환(cudoc: cyclic embed: a#* -> b#limits -> a#*) 제거
컴포넌트의 공통 렌더러가 없음 Markdown 사용, 정적 의미 매핑 설정, 또는 렌더링 API에 렌더러 전달
내용은 맞지만 링크가 틀림 routeBase, routeSuffix, routes를 실제 호스트 경로에 맞춤
no documents in folder … 폴더에 그 폴더를 대표하는 페이지 말고는 문서가 없음. 문서를 더하거나 경로와 exclude 확인
… a folder source ends with / 원본이 폴더를 슬래시 없이 가리킴. 폴더의 문서를 나열하려면 guides/처럼 씀
… holds only private documents … 공개 페이지는 비공개 문서를 나열하지 않음. 트리를 비공개 페이지에 쓰거나 문서를 원본에 직접 적음
트리가 상자 모양으로 그려짐 호스트 가이드대로 @cudoment/cudoc/styles.css를 가져옴
줄이 아래 줄의 요약을 되풀이함 절의 첫 하위 제목 앞에 문단을 두거나 columns: [link] 사용
unmatched-tree-order 항목을 파일 이름이나 줄에 보이는 제목으로, 대소문자까지 같게 적음. 대괄호 목록에서는 쉼표가 이름을 둘로 나누므로 쉼표가 든 이름을 따옴표로 감쌈
render.order must be a list of names : 가 든 이름을 따옴표로 감쌈

임베드는 ID와 참조를 조정해 충돌을 피하고 문서 링크와 이미지 경로를 다시 계산합니다. 복사한 절 안에서 #setup이나 ?tab=2처럼 조각이나 쿼리만 쓴 링크는 원래 쓰인 문서를 계속 뜻합니다. 복사본에 그 앵커가 있으면 복사본 안을, 없으면 원본 문서를 가리킵니다. 코드로 직접 수집하거나 비동기 컴파일러, 저장 데이터, 수동 렌더링을 다루는 방법은 Node API 레퍼런스에서 설명합니다.

cudoc