본 포스팅은 실제 업무 과정에서 진행한 Docusaurus + Cloudflare Pages 자동 배포 파이프라인 구축 기록입니다. 날짜와 프로젝트명, 도메인 등은 일부 마스킹 처리하였으며, 문제 해결 과정에서 얻은 인사이트를 중심으로 서술합니다.
1. 배경과 목표
팀에서 Bruno(.bru) 파일로 관리하던 API 컬렉션을 Docusaurus 기반 문서 사이트로 변환하는 작업이 진행 중이었다. 기존에는 .bru 파일에 docs {} 블록을 수동으로 작성하고, 로컬에서 npm run generate && npm run build를 돌려 빌드하는 방식이었다.
최종 목표는 다음과 같았다:
main브랜치에.bru파일이 푸시되면 문서가 자동으로 재생성 및 배포된다.- 사이드바와 제목이 한글 기준으로 일관되게 노출된다.
- API 도메인이 많아질 경우 의미 그룹으로 재분류되어 탐색이 용이하다.
- 배포는 Cloudflare Pages를 활용하고, 남은 설정은 최소화한다.
2. 작업 순서 및 문제 해결
Step 0. 워크트리 활성화 및 로컬 검증
기존 브랜치(feat/docusaurus-sync)는 별도 git worktree로 분리되어 있었다. 작업 전 npm test(17/17 pass)와 npm run build를 통해 로컬 환경이 정상임을 확인했다.
교훈: 배포 관련 작업을 시작하기 전, 반드시 로컬 빌드가 깨지지 않는 상태를 유지해야 한다. 이후의 모든 변경은 이 기준 위에서 이루어진다.
Step 1. Broken Link 수정 — Changelog 링크 경로
빌드 시 broken link 경고가 발생했다. 원인은 docs/api/notices/notices.mdx처럼 폴터명과 파일명이 동일한 경우, Docusaurus가 해당 페이지를 도메인의 index 페이지로 취급하여 URL이 /api/notices/가 되기 때문이었다.
반면 changelog 생성 스크립트는 /api/notices/notices로 링크를 생성하고 있어 경로 불일치가 생겼다.
해결:
const linkPath = item.domain === item.slug
? `/api/${item.domain}/`
: `/api/${item.domain}/${item.slug}`;
domain === slug일 경우 index 경로(/api/{domain}/)를 사용하도록 분기 처리했다.
교훈: Docusaurus에서 폴터명과 파일명이 같으면 슬러그가 자동으로 index가 된다. 링크 생성 로직은 이 케이스를 반드시 처리해야 한다.
Step 2. 이모지/이모티콘 전면 제거
프로젝트 전체에서 이모지 사용을 지양하기로 결정했다. 사이드바(📱 화면, 🔌 API 레퍼런스, 📚 용어집), generate 스크립트 콘솔 출력(✅, ⚠️), .bru 파일의 docs{} 블록 난내 화살표(→) 등을 모두 제거했다.
특히 .bru 난내 화살표는 자연어 전환으로 대체했다.
- Before:
`min_level IS NULL` → `"레벨 무관"` - After:
`min_level IS NULL`일 때 `"레벨 무관"`
교훈: 이모지는 터미널/CI 로그, 문서 정돈성, 검색 인덱싱 측면에서 예상치 못한 부작용을 낳는다. 팀 컨벤션으로 확정했다면 일관성 있게 전면 제거해야 한다.
Step 3. API 문서 제목 간소화
.bru 파일의 meta.name은 /api/v3/friends/requests/received/{id}/accept/ - 받은 친구 요청 수락처럼 엔드포인트 경로 + 한글 설명이 붙어 있었다. 이 값을 그대로 MDX의 title에 쓰니 제목이 지나치게 길어졌다.
해결:
const shortTitle = meta.name.split(' - ')[1] ?? meta.name;
sidebar_label은 이미 이렇게 처리하고 있었으므로 title도 동일하게 적용했다. 본문 상단에 <code> 태그로 method + URL이 표시되므로 정보 손실은 없다.
교훈: 문서 제목은 식별 용도로 충분할 정도로 간결해야 한다. 본문에서 이미 표시되는 정보를 제목에서 중복하면 가독성이 떨어진다.
Step 4. 도메인 사이드바 라벨 통일 — _category_.json
Docusaurus는 폴터명을 그대로 사이드바에 노출한다. notices 도메인은 notices/notices.mdx 구조 때문에 페이지의 sidebar_label("공지사항 목록")이 직접 노출되었고, 나머지 도메인은 영문(accounts, auth 등)으로 나와 불일치가 생겼다.
해결:
각 도메인 폴터에 _category_.json을 자동 생성했다.
{
"label": "공지사항"
}
이제 모든 도메인이 한글로 통일되었다.
교훈: autogenerated 사이드바를 쓸 때는 _category_.json이 사이드바 UX의 핵심이다. 특히 폴터명==파일명인 경우 Docusaurus의 기본 동작이 달라질 수 있으므로 명시적인 라벨링이 필수다.
Step 5. accounts 도메인 6개 서브그룹 재분류
accounts 도메인은 22개 엔드포인트로 사이드바가 너무 길었다. 의미 기준으로 6개 서브그룹으로 나누었다.
| 서브그룹 | 라벨 | 예시 엔드포인트 |
|---|---|---|
| basic | 기본 정보 | get-account-me, post-withdrawal |
| profile | 프로필 | get-profile-basic, patch-profile-image |
| privacy | 개인정보 / 결제 | get-privacy, patch-payment-info |
| favorites | 선호 설정 | get-favorite-areas, post-favorite-stadium |
| notifications | 알림 | get-notification-status |
| verification | 휴게폰 인증 | post-phone-verification-code |
구현:generate.ts에 ACCOUNTS_SUBGROUPS 매핑을 추가하고, accounts 도메인일 때만 join(OUT_DIR, domain, subgroup) 경로로 MDX를 생성하도록 처리했다. 이후 _category_.json도 서브그룹 단위로 생성했다.
주의사항: 기존 폴터 루트에 중복 파일이 남아 broken link가 발생할 수 있으므로, 재생성 전에 docs/api/accounts/의 루트 MDX를 정리해야 한다.
교훈: 15개 이상의 엔드포인트가 한 도메인에 모여 있으면 반드시 서브그룹화를 고려해야 한다. _category_.json은 중첩 폴터에도 동일하게 적용된다.
Step 6. main 브랜치 동기화
main 브랜치에서 .bru 파일 대량 문서화가 진행되어 있었다. 이를 머지하니 lint warnings가 48개에서 0개로 사라졌고, 새로운 get-recommended-matches.bru가 추가되어 API 페이지가 49개 → 50개가 되었다.
merge conflict는 matches/get-social-matches.bru의 관련 티켓 섹션에서 발생했다. main의 변경사항을 수용하되, 이모지 제거 원칙은 유지했다.
교훈: 문서화 작업이 병렬로 진행될 때는 주기적으로 main을 머지하여 drift를 방지해야 한다. 충돌은 대개 docs {} 블록의 텍스트 영역에서 발생한다.
Step 7. GitHub Actions 자동 배포 설정
.github/workflows/docs-build.yml을 세팅했다. 핵심은 다음과 같다.
on:
push:
branches: [main]
paths:
- '**/*.bru'
- 'docs-site/**'
- 'environments/**'
- '.github/workflows/docs-build.yml'
paths 필터를 추가하여 불필요한 배포를 방지했다. 예컨대 README 수정만으로 문서가 재배포되는 일은 없다.
교훈: paths 필터는 CI 시간과 Cloudflare 빌드 횟수를 절약한다. 특히 정적 사이트 배포에서는 매우 중요하다.
Step 8. Cloudflare Pages 프로젝트 생성
대시보드에서 pf-api-docs라는 이름으로 Pages 프로젝트를 생성했다. 처음에는 Workers 탭에서 생성했는데, 이는 .workers.dev 도메인을 가지는 함수 프로젝트였다. 정적 사이트 배포에는 반드시 Pages 탭에서 Direct Upload로 생성해야 한다.
생성 후 "Upload assets" 버튼은 누르지 않았다. GitHub Actions가 wrangler pages deploy로 배포할 예정이므로, 프로젝트 존재 자체만 확보하면 된다.
교훈: Cloudflare의 Workers와 Pages는 UI가 유사하지만 완전히 다른 제품이다. Pages 배포를 위해서는 반드시 Pages 탭에서 프로젝트를 생성해야 한다.
Step 9. GitHub Secrets 등록
GitHub 레포지토리 Settings → Secrets → Actions에 두 개의 Secret을 등록했다.
CLOUDFLARE_API_TOKEN: Cloudflare Profile → API Tokens → "Edit Cloudflare Pages" 템플릿CLOUDFLARE_ACCOUNT_ID: Cloudflare Dashboard 우측에 표시되는 Account ID
Step 10. PR 생성 → 머지 → 첫 배포 시도
feat/docusaurus-sync → main PR을 생성하고 머지했다. GitHub Actions가 트리거되었으나 두 차례 실패가 발생했다.
실패 1: package-lock.json 누락
##[error]Some specified paths were not resolved, unable to cache dependencies..gitignore에 package-lock.json이 전역 무시되어 있어 GitHub Actions의 cache: 'npm'이 작동하지 않았다. .gitignore에서 해당 규칙을 제거하고 docs-site/package-lock.json을 커밋했다.
교훈: actions/setup-node의 cache: 'npm' 옵션은 반드시 커밋된 package-lock.json을 필요로 한다. .gitignore에 포함되어 있으면 캐시 로직이 깨진다.
실패 2: Peer dependency 충돌
npm error peer react@"^16.14.0 || ^17 || ^18" from @easyops-cn/docusaurus-search-local@0.44.1Docusaurus와 검색 플러그인의 React 버전 호환성 문제였다.
해결:
- run: npm ci --legacy-peer-deps
--legacy-peer-deps 플래그를 추가하여 peer dependency 충돌을 우회했다.
교훈: Docusaurus 생태계의 플러그인들은 React 버전 peer dependency가 엄격한 경우가 많다. CI 환경에서는 --legacy-peer-deps를 고려해야 한다.
Step 11. 배포 성공 확인
세 번째 시도에서 빌드가 성공했다.
- GitHub Actions:
Build & Deploy Docs✅ - Cloudflare Pages:
pf-api-docs.pages.dev접속 확인 ✅ - Docusaurus 사이트 타이틀: "Plab 2.0 API Docs" 정상 출력 ✅
3. 최종 아키텍처
[GitHub: main 브랜치]
↓ .bru 파일 push (paths 필터)
[GitHub Actions: docs-build.yml]
1. checkout (fetch-depth: 0)
2. setup-node (cache: npm)
3. npm ci --legacy-peer-deps
4. npm run generate (.bru → .mdx)
5. npm run build (Docusaurus 정적 빌드)
6. wrangler pages deploy → Cloudflare Pages4. Lessons Learned (레슨런)
4.1 Docusaurus 링크 동작의 함정
- 폴터명 == 파일명인 경우 자동으로 index 페이지가 된다. 이 경우 URL이 달라지므로 링크 생성 로직에서 반드시 예외 처리해야 한다.
slugfrontmatter를 추가하면 전체 링크 구조가 의도치 않게 바뀔 수 있다. 추가 전에 전체 링크에 미치는 영향을 테스트해야 한다.
4.2 _category_.json은 UX의 기초
autogenerated사이드바를 사용할 때, 폴터마다_category_.json으로 한글 라벨을 명시해야 일관된 탐색 경험을 제공할 수 있다.- 특히 중첩 폴터(accounts/basic, accounts/profile 등)를 사용할 때 더욱 중요하다.
4.3 CI/CD에서의 lockfile 관리
package-lock.json은.gitignore에 넣지 말아야 한다. GitHub Actions의 npm 캐싱과 재현 가능한 빌드를 위해 필수다.- Docusaurus + 플러그인 조합에서는
--legacy-peer-deps가 필수일 수 있다. 로컬에서npm install할 때와 CI 환경의 차이를 주의해야 한다.
4.4 Cloudflare Workers vs Pages
- 정적 사이트 배포는 반드시 Pages에서 프로젝트를 생성해야 한다.
- Workers는 함수 기반이며
.workers.dev도메인을 가진다. Pages는pages.dev를 가진다. wrangler pages deploy는 Pages 프로젝트를 대상으로 한다.
4.5 paths 필터의 중요성
.bru나 문서 관련 파일 외의 변경(예:README.md)으로 문서가 재배포되지 않도록paths필터를 명시했다.- 이는 CI 크레딧, 빌드 시간, Cloudflare 배포 횟수를 모두 절약한다.
4.6 병렬 문서화 작업의 충돌 관리
.bru파일의docs {}블록은 동시에 여러 사람이 편집하면 충돌이 빈번하다.- 주기적으로
main을 머지하여 drift를 방지하고, 충돌 시에는 텍스트 콘텐츠를 우선적으로 수용하되 팀 컨벤션(이모지 제거 등)은 유지해야 한다.
5. 남은 작업
- Cloudflare Access: 내부 이메일 도메인 제한
'🌈 프로그래밍 > TIL' 카테고리의 다른 글
| MySQL FLUSH HOSTS (0) | 2026.04.20 |
|---|---|
| ElastiCache Redis Read/Write Splitting 적용기 - 장애에서 배운 이중화 전략 (0) | 2026.03.05 |
| Claude Code에서 MySQL MCP 서버 SSL 연결 문제 해결하기 (1) | 2026.01.09 |
| Django ORM aggregate Sum이 NULL을 반환할 때 (1) | 2025.12.19 |
| Django ORM __in 조회에 딕셔너리 전달하면 어떻게 될까? (0) | 2025.12.18 |