FastAPI로 Docusaurus와 유사한 사이트 만들기: 6단계 - 사이드바 생성
Daniel Hayes
Full-Stack Engineer · Leapcell

이전 글에서 Markdown 내의 (이미지와 같은) 스태틱 리소스 로딩 문제를 해결했습니다.
지금까지 문서 페이지는 내용을 표시하고, 코드를 하이라이트하고, 이미지를 멋지게 표시할 수 있었습니다. 하지만 독자들은 여전히 문서를 탐색하는 데 어려움을 겪고 있습니다. 페이지들이 각각 고립된 섬과 같습니다. 수동으로 URL을 입력하지 않으면 한 기사에서 다른 기사로 점프할 수 없습니다.
Docusaurus와 같은 문서 사이트는 일반적으로 "왼쪽 사이드바 + 오른쪽 콘텐츠" 레이아웃을 사용합니다.
이 글에서는 이 기능을 구현할 것입니다. docs/ 디렉토리의 모든 Markdown 파일을 자동으로 스캔하고, 해당 제목을 추출하고, 동적으로 사이드바 내비게이션 메뉴를 생성하는 함수를 작성합니다.
1단계: 사이드바 레이아웃 스타일 만들기
먼저 페이지 레이아웃을 원래의 "단일 열 수직 구조"에서 "이중 열 수평 구조"로 변경해야 합니다.
이 레이아웃을 정의하기 위한 새 CSS 파일이 필요합니다. static/css/ 디렉토리에 layout.css 파일을 만듭니다.
업데이트된 파일 구조:
static/
└── css/
├── highlight.css
└── layout.css <-- 새로 추가
static/css/layout.css 편집:
/* 전역 리셋 및 기본 스타일 */ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; color: #333; } /* 메인 컨테이너: Flexbox를 사용하여 나란히 레이아웃 구현 */ .main-container { display: flex; min-height: 100vh; } /* 왼쪽 사이드바 스타일 */ .sidebar { width: 250px; background-color: #f4f4f4; border-right: 1px solid #ddd; padding: 20px; flex-shrink: 0; /* 사이드바가 압축되지 않도록 방지 */ } .sidebar h3 { margin-top: 0; font-size: 1.1rem; color: #555; } .sidebar ul { list-style: none; padding: 0; } .sidebar li { margin-bottom: 10px; } .sidebar a { text-decoration: none; color: #333; font-size: 0.95rem; } .sidebar a:hover { color: #007bff; } /* 오른쪽 콘텐츠 영역 스타일 */ .content { flex-grow: 1; padding: 20px 40px; max-width: 800px; /* 더 나은 가독성을 위해 콘텐츠 최대 너비 제한 */ }
2단계: HTML 템플릿 수정
다음으로 templates/doc.html을 수정하여 새 CSS 파일을 포함하고 사이드바를 수용하도록 HTML 구조를 조정합니다.
템플릿에 sidebar_items라는 새 변수를 도입할 것입니다. 이것은 문서 목록을 포함하는 배열이며, 나중에 Python 코드에서 전달됩니다.
templates/doc.html 수정:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>{{ page_title }} - My Docs Site</title> <link rel="stylesheet" href="{{ url_for('static', path='css/highlight.css') }}" /> <link rel="stylesheet" href="{{ url_for('static', path='css/layout.css') }}" /> </head> <body> <div class="main-container"> <aside class="sidebar"> <h3>Contents</h3> <ul> {% for item in sidebar_items %} <li> <a href="{{ item.url }}">{{ item.title }}</a> </li> {% endfor %} </ul> </aside> <main class="content"> <h1>{{ page_title }}</h1> <hr /> <div class="doc-content">{{ content | safe }}</div> </main> </div> </body> </html>
3단계: 디렉토리 스캔 로직 구현
이제 docs/ 폴더를 스캔하고 모든 .md 파일을 찾아서 해당 Frontmatter를 파싱하여 제목을 가져오는 Python 코드를 작성해야 합니다.
main.py를 엽니다. pathlib 라이브러리를 가져와야 합니다 ( os를 사용할 수도 있지만, pathlib이 더 현대적입니다) 그리고 헬퍼 함수를 작성합니다.
main.py 수정:
# main.py from fastapi import FastAPI, Request from fastapi.templating import Jinja2Templates from fastapi.responses import HTMLResponse import markdown from fastapi.staticfiles import StaticFiles import frontmatter from pathlib import Path # 1. Path 가져오기 app = FastAPI() app.mount("/static", StaticFiles(directory="static"), name="static") app.mount("/docs/assets", StaticFiles(directory="docs/assets"), name="doc_assets") templates = Jinja2Templates(directory="templates") # --- 헬퍼 함수: 사이드바 데이터 생성 --- def get_sidebar_items(): items = [] docs_path = Path("docs") # docs 디렉토리의 모든 .md 파일 반복 for file_path in docs_path.glob("*.md"): # Frontmatter를 파싱하여 제목 가져오기 try: post = frontmatter.load(file_path) # 제목이 없으면 파일 이름 사용 title = post.metadata.get("title", file_path.stem) except: title = file_path.stem # URL 생성, 규칙은 /docs/{filename}으로 가정 # file_path.stem는 확장자가 없는 파일 이름 (예: "hello")을 가져옵니다. url = f"/docs/{file_path.stem}" items.append({"title": title, "url": url}) # 제목별로 정렬 (또는 정렬되도록 'order' 필드를 추가할 수 있음, 여기서는 단순히 제목별로 정렬) items.sort(key=lambda x: x["title"]) return items @app.get("/", response_class=HTMLResponse) async def root(request: Request): # 홈페이지는 현재 변경되지 않음, 나중에 사이드바를 추가할 수 있습니다. context = { "request": request, "page_title": "Hello, Jinja2!" } return templates.TemplateResponse("index.html", context) @app.get("/docs/hello", response_class=HTMLResponse) async def get_hello_doc(request: Request): md_file_path = "docs/hello.md" try: post = frontmatter.load(md_file_path) except FileNotFoundError: return HTMLResponse(content="<h1>404 - Document Not Found</h1>", status_code=404) except Exception as e: return HTMLResponse(content=f"<h1>500 - Parse Error: {e}</h1>", status_code=500) metadata = post.metadata md_content = post.content extensions = ['fenced_code', 'codehilite'] html_content = markdown.markdown(md_content, extensions=extensions) page_title = metadata.get('title', 'Untitled Document') # 2. 사이드바 데이터 가져오기 sidebar_items = get_sidebar_items() context = { "request": request, "page_title": page_title, "content": html_content, "sidebar_items": sidebar_items # 3. 템플릿에 전달 } return templates.TemplateResponse("doc.html", context)
4단계: 테스트를 위한 두 번째 문서 추가
사이드바가 실제로 작동하는지 확인하기 위해 두 번째 Markdown 파일이 필요합니다.
docs/ 디렉토리에 setup.md를 생성합니다:
--- title: 환경 설정 가이드 author: Leapcell date: 2025-11-10 --- # 프로젝트 환경 설정 이것은 우리의 두 번째 문서입니다. 1. Python 설치 2. FastAPI 설치 3. 코드 실행
5단계: 실행 및 테스트
uvicorn main:app --reload를 실행하여 서버를 시작합니다.
http://127.0.0.1:8000/docs/hello를 방문합니다.
다음과 같은 변경 사항이 표시됩니다.:
- 페이지 레이아웃이 왼쪽 사이드바 네비게이션과 오른쪽 콘텐츠 영역으로 변경되었습니다.
- 왼쪽 사이드바에 "Hello, Frontmatter!" 및 "환경 설정 가이드"라는 두 개의 링크가 자동으로 나열됩니다.

하지만 작은 문제가 있습니다:
사이드바에서 "환경 설정 가이드"를 클릭하면 브라우저가 /docs/setup으로 점프합니다. 이때 콘텐츠를 찾을 수 없다는 오류가 표시됩니다.
이것은 main.py에 현재 하드코딩된 /docs/hello 경로만 있고 /docs/setup을 아직 처리하지 않기 때문입니다.
요약
파일 시스템을 스캔하여 문서 사이트에 목차를 자동으로 생성하는 기능을 추가했습니다. docs/ 폴더에 .md 파일을 몇 개 추가하거나 제거하더라도 사이드바는 자동으로 업데이트됩니다.
사이드바는 스마트하지만 라우팅은 "서투릅니다" — 경로 주소가 하드코딩되어 있기 때문입니다. 문서가 100개 있다면, main.py에 100개의 @app.get("/docs/xxx") 함수를 작성할 수는 없을 것입니다. 이것은 명백히 실행 가능한 방법이 아닙니다.
우리는 /docs/{any_filename}에 대한 요청을 캡처하고 해당 Markdown 파일을 자동으로 찾는 일반적인 방법이 필요합니다.
다음 글에서 경로 기반 동적 라우팅을 구현하여 404 오류 문제를 완전히 해결하고 사이드바의 모든 링크가 해당 기사로 올바르게 이동하도록 할 것입니다.
기타
사이트를 구축한 후에는 온라인에 배포하여 다른 사람들이 볼 수 있도록 하고 싶을 수 있습니다. 하지만 대부분의 클라우드 플랫폼은 비싸고, 이와 같은 연습 프로젝트에 높은 가격을 지불할 가치는 없습니다.
더 경제적인 배포 방법이 있을까요? Leapcell을 사용해 볼 수 있습니다. Python, Node.js, Go, Rust와 같은 다국어 배포를 지원하며 매달 넉넉한 무료 티어를 제공하여 한 푼도 지출하지 않고 최대 20개의 프로젝트를 배포할 수 있습니다.

