단독 설계·개발 (아키텍트 + 풀 개발) · 2026.05 -

PNU-Modu - 서버 없는 Git 기반 협업 CMS

비전공 교직원이 마크다운 한 줄로 인쇄물 수준의 디지털 브로슈어를 만들고, 저장 한 번이면 자동 배포되는 무설치형 CMS. GitHub API와 git 커밋만으로 인증·협업 잠금·CI/CD를 구현했으며, 단독으로 설계·개발했습니다.

  • React 19
  • Docusaurus 3
  • MDX
  • Vanilla JS
  • GitHub REST API
  • GitHub Actions
  • GitHub Pages

개요

부산대 AI융합교육원의 온라인 디지털 브로슈어를, 비전공 교직원이 코드를 몰라도 직접 제작·배포할 수 있게 만든 무설치형 협업 CMS입니다. 부트캠프·대학원·교원 연수 등 개별 교육과정 브로슈어를 한곳에서 잇는 PNU AI Hub의 일부로, 각 과정은 별도 저장소로 관리하고 허브가 중앙에서 연결합니다.

원장님의 “교직원 모두가 마크다운과 GitHub를 다룰 수 있어야 한다”는 방침에서 출발했습니다. 하지만 “개발자인 나조차도 GitHub는 여전히 복잡하고 마크다운 문법도 매번 검색해서 쓰는데, 비전공 연구원분들이 이걸 직접 다루는 게 과연 가능할까?“라는 의문이 들었습니다. 그래서 문제를 다시 정의했습니다 - 방침의 의도(누구나 콘텐츠를 직접 관리)는 그대로 살리되, 비전공자가 GitHub와 마크다운을 실제로는 사용하지만 사용하는 줄 모르도록 그 복잡성을 완전히 숨기는 방향으로 설계했습니다. CMS 뒤에서는 git 커밋이 일어나지만 사용자에게는 “저장 버튼”으로, 마크다운 뒤에는 ::: 디렉티브가 있지만 사용자에게는 “블록 한 줄과 실시간 미리보기”로 보이게 만든 것입니다.

지시받은 과업이 아니라 스스로 판단해 만들어 공개했고, 실장님과의 여러 차례 미팅을 거쳐 도입 직전 단계까지 추진됐습니다. (브로슈어 제작 타이밍이 아직 아니어서 정식 운영 사용은 그 전 단계입니다.)

실제 배포된 브로슈어 - 마크다운으로 작성한 hero·KPI 블록이 렌더된 모습. 상단 네비게이션 탭은 문서 폴더에서 자동 생성되며, 우측 상단 “관리” 버튼으로 CMS에, “PDF 저장” 버튼으로 A4 인쇄로 이어집니다.

주요 기여

시연 영상

관리자 페이지에서 브로슈어를 편집하고, 블록을 배치하고, 실시간으로 미리보며 저장하는 전체 흐름입니다.

어떻게 동작하나

관리자 페이지에서의 편집이 곧 git 커밋이 되고, 커밋이 CI/CD를 돌려 사이트로 배포되는 Decoupled Git-based CMS 구조입니다.

[교직원 / 편집자]
   │  /admin 에서 마크다운·블록 편집

[Web CMS · static/admin/index.html]     ← 무프레임워크 단일 파일 앱
   │  GitHub REST API (PUT/DELETE contents)

[GitHub 저장소 (main)]                    ← 콘텐츠 + .locks(잠금) + 커밋 히스토리(감사로그)
   │  push 트리거

[GitHub Actions · Node 20]               ← npm build → Docusaurus SSG


[GitHub Pages] → 라이브 온라인 브로슈어

이 구조 덕분에 운영자는 로컬 개발환경 설치가 전혀 필요 없고, 저장 버튼 한 번으로 편집부터 배포까지 끝납니다. 콘텐츠·협업 잠금·감사 추적을 전부 git 하나로 통합했습니다.

핵심 기능 들여다보기

서버리스 Git 기반 CMS

관리자 페이지는 별도 프레임워크·번들러·백엔드 없이 순수 JavaScript로 작성한 단일 HTML 파일입니다(마크다운 편집기로는 Toast UI Editor를 사용). GitHub Personal Access Token으로 로그인하고, 조직(org) 멤버십을 확인해 권한 없는 접근을 차단합니다. 모든 편집은 GitHub Contents API 커밋으로 변환되어 곧바로 CI/CD를 트리거하며, 누가 어떤 문서를 생성·수정·삭제했는지가 커밋 히스토리에 그대로 남아 별도 로그 없이 감사 추적(Audit Trail)이 됩니다. 파일 목록은 캐시가 끼는 Contents API 대신 Git Trees/Blobs API로 읽어 항상 최신 상태를 보장했습니다.

Soft Lock 분산 협업 잠금

GitHub Contents API에는 파일 잠금 기능이 없습니다. 서버도 두지 않기로 했기 때문에, 저장소 자체를 분산 락 매니저로 활용했습니다. .locks/{문서ID}.lock.json 파일을 커밋해 잠금을 표현하고, 5분마다 heartbeat로 갱신, 15분간 갱신이 없으면 죽은 락으로 보고 자동 인수해 크래시를 복구합니다. 락 관련 커밋에는 [skip ci]를 붙이고 워크플로에도 paths-ignore: ['.locks/**']를 걸어, 조율용 트래픽이 사이트 배포를 일으키지 않도록 컨트롤 플레인과 데이터 플레인을 분리했습니다.

→ 자세히: git 커밋으로 만든 서버 없는 분산 편집 잠금(Soft Lock)

::: 디렉티브 DSL

비전공자가 React·JSX를 몰라도 인쇄물 수준의 섹션을 만들 수 있도록, 마크다운 ::: 문법을 React 컴포넌트로 변환하는 자체 도메인 특화 언어를 remark-directive 기반 커스텀 플러그인으로 설계했습니다. 통계·로드맵·전략 프레임워크 등 디렉티브 15종과 그에 대응하는 블록 컴포넌트 17종을 만들었고, 콘텐츠와 디자인을 완전히 분리했습니다. 사용자 입력 오류로 빌드가 죽지 않도록 정식 YAML 파서 대신 관용 파서를 직접 작성한 것도 이 과정의 일부입니다.

운영자는 이렇게 씁니다. 아래는 실제 작성 문법과 그 렌더 결과를 나란히 둔 것입니다. 가장 단순한 히어로 배너는 속성 몇 줄이 전부입니다.

:::hero
badge: CHAPTER 01 · 사업소개
title: 진정한 AI·SW 가치확산을 실현하는 부산대학교 SW중심대학
description: 대학교육을 SW중심으로 혁신해 학생·기업·사회의 경쟁력을 강화합니다.
:::

위 :::hero 문법이 렌더된 결과 - 배지·제목·설명이 그대로 히어로 배너가 됩니다.

목록이 필요한 블록은 -로 항목을 나열합니다. 다음은 4C 추진전략 블록의 실제 소스입니다.

:::strategy
title: 4C 추진전략
subtitle: STRATEGIC FRAMEWORK

- letter: C
  title: Customized Education
  desc: 신기술 반영 교육과정 기반 맞춤형 교육혁신

- letter: C
  title: Convergence Research
  desc: 미래지향적 융합 연구 및 성과 창출
:::

위 :::strategy 문법이 렌더된 결과 - letter·title·desc 항목이 카드 그리드가 됩니다.

규칙은 이게 전부입니다 - :::블록이름으로 열고 :::로 닫기, 속성은 key: 값, 목록은 -로 시작. JSX도 import도 없습니다.

이 텍스트가 React 컴포넌트가 되기까지

:::strategy (마크다운)
  → remark-directive 가 containerDirective 노드로 파싱
  → 커스텀 플러그인이 본문을 key:value·목록으로 읽고, items를 Base64로 감싼 뒤
    노드에 hName='strategy' 와 hProperties(속성)를 붙임
  → MDX 가 <strategy .../> 로 변환
  → MDXComponents 매핑이 그걸 StrategySection 컴포넌트로 치환
  → StrategySection 이 items를 디코딩해 그리드로 렌더

디렉티브 이름과 컴포넌트를 잇는 곳은 딱 두 군데입니다.

// remarkBrochureDirectives.js - 디렉티브 이름 → 태그 이름
data.hName = nameMap[node.name] || node.name;   // 'strategy'
data.hProperties = { ...node.attributes };       // title, subtitle, items(Base64)

// MDXComponents.js - 태그 이름 → 실제 React 컴포넌트
strategy: StrategySection,

목록을 Base64로 감싸는 건, 배열을 그대로 HTML 속성에 실으면 따옴표·중괄호가 JSX 직렬화에서 깨지기 때문입니다. 인코딩해 안전하게 넘기고 컴포넌트 안에서 다시 풉니다.

실시간 WYSIWYG 프리뷰

저장 전에 결과물을 정확히 확인할 수 있어야 했지만, MDX는 빌드가 필요해 브라우저에서 즉시 렌더할 수 없습니다. 그래서 프리뷰를 별도 Docusaurus 라우트로 만들고 iframe + postMessage로 에디터와 연결했습니다. 프로덕션과 동일한 컴포넌트 트리를 react-markdown으로 실시간 렌더해 미리보기가 곧 배포물이 되게 했고, 프리뷰의 블록을 클릭하면 에디터의 해당 줄로 점프하고 더블클릭하면 그 자리에서 인라인 편집이 되도록 했습니다. 왼쪽 텍스트와 오른쪽 렌더 화면이 서로를 가리키고 고치는 이 양방향 편집이, 비전공자가 마크다운을 의식하지 않게 만드는 이 프로젝트의 핵심 기능입니다.

→ 자세히: 마크다운 에디터와 미리보기를 양방향으로 잇기 - data-source-line과 postMessage

A4 PDF / 인쇄 시스템

디지털 브로슈어를 실제 A4 인쇄물로도 출력해야 했습니다. 서버 렌더러 없이 순수 인쇄 CSS(@page / @media print)만으로, 210mm 안에 178mm 고정 컬럼을 잡고 UI를 모두 숨기며 배경색·그라데이션까지 보존되도록 구현했습니다. 브라우저마다 다른 인쇄 엔진의 컬럼 붕괴를 여러 차례 반복하며 잡았습니다.

빌드 타임 자동화

챕터 목록과 네비게이션 탭을 수동 관리하지 않도록, 빌드 전에 docs/ 폴더의 문서들을 스캔해 챕터 데이터와 네비게이션을 자동 생성하게 했습니다. 마크다운 파일을 추가하기만 하면 탭과 홈 카드가 자동으로 만들어집니다.

기술적 도전

이 프로젝트에서 가장 어려웠던 건, 서버라는 안전지대 없이 GitHub 하나만으로 인증·동시성 제어·배포를 전부 만들어야 한다는 점이었습니다. 파일 잠금이 없는 API 위에서 동시 편집을 막고, 콘텐츠가 아닌 조율 신호가 배포를 폭발시키지 않게 나누는 문제는, 도구가 GitHub뿐이어도 결국 분산 시스템이 다루는 문제와 같았습니다.

각 사건은 따라 하며 재현할 수 있게 정리하고 있습니다.

DSL 파이프라인(::: 디렉티브), 인쇄 CSS(A4 PDF), 캐시 우회 읽기 등 나머지 케이스스터디는 이어서 작성 중입니다.

배운 점

혼자 결정하고 혼자 만든 프로젝트였지만, 그래서 오히려 “누구를 위해, 왜 만드는가”를 계속 되물어야 했습니다. 기술을 화려하게 드러내는 것보다 사용자가 복잡함을 아예 느끼지 않도록 숨기는 것이 진짜 사용성이라는 것, 그리고 서버가 없어도 공유 상태와 동시성 제어는 만들 수 있다는 것을 이 프로젝트에서 배웠습니다. 시키지 않은 일을 스스로 정의해 도입 직전까지 끌고 간 경험은, 문제를 발견하고 끝까지 책임지는 방식이 무엇인지 알게 해주었습니다.