Notion + GitHub로 개발 문서 자동화하기
아이디어가 떠오르면 Notion에 적고, 검토가 끝나면 GitHub로 자동 반영되어 사이트에 배포된다면 얼마나 편할까요? 이 문서에서는 Notion을 콘텐츠 소스로, GitHub를 검토·버전관리·배포 허브로 사용해 개발 문서를 자동화하는 설계를 처음부터 끝까지 안내합니다.
개요
- 문서 자동화의 목표와 전반 구조
- Notion 준비: 데이터 모델과 권한
- GitHub 준비: 저장소 레이아웃과 배포 대상
- 데이터 흐름 패턴: 단방향 vs. 양방향
- 구현 A안(가장 단순): 스케줄 동기화
- 구현 B안(변경 감지): 수동 트리거/PR 기반
- 콘텐츠 변환: Markdown, 프론트매터, 이미지
- 품질 게이트: 링크·맞춤법·스타일 체크
- 운영 팁: 권한, 보안, 성능, 로그
- 문제 해결: 자주 발생하는 오류와 대응
- 마이그레이션 & 확장: Docusaurus/MkDocs/Next
- 결론 요약 및 다음 단계
- FAQ (3)
문서 자동화의 목표와 전반 구조
개발 문서 자동화의 핵심은 한 곳에서 쓰고(작성), 다른 곳에서 자동으로 보여주는(배포) 것입니다. Notion은 작성 경험이 좋고, GitHub는 버전관리와 자동 배포에 최적화되어 있습니다. 두 도구를 연결하면 “작성 → 검토 → 배포”가 자연스러운 파이프라인으로 이어집니다.
아키텍처 요약
- 작성: Notion 데이터베이스에서 페이지 작성/수정
- 동기화: GitHub Actions가 Notion API로 콘텐츠를 가져와 Markdown으로 변환
- 검토: Pull Request로 리뷰, 체크 통과 시 main에 병합
- 배포: docs 프레임워크(Docusaurus/MkDocs/Next)로 사이트 자동 배포
왜 이 조합인가
- 팀이 이미 Notion을 사용 중이라면 러닝커브가 낮습니다.
- GitHub는 리뷰, 이력, 롤백, 권한 관리가 강력합니다.
- 자동화(CI)로 일관성 있는 품질 게이트를 적용할 수 있습니다.
Notion 준비: 데이터 모델과 권한
먼저 문서를 관리할 데이터베이스를 만듭니다. 페이지를 무분별하게 늘리지 않도록, 최소한의 필드를 표준화하세요.
| 필드 | 타입 | 설명/예시 |
| Title | Title | 문서 제목 |
| Slug | Text | 문서 경로(ex. guides/notion-github-sync) |
| Status | Select | Draft / Review / Published |
| Category | Select | Guide / Concept / Reference 등 |
| Tags | Multi-select | api, actions, docusaurus... |
| Visibility | Select | Public / Internal |
| Owner | People | 작성/책임자 |
| Updated | Date | 최종 수정 시각 |
- Notion에서 데이터베이스를 만들고 위 필드를 추가합니다.
- Notion 내 통합(Integration)을 생성해 비밀 토큰을 발급받습니다.
- 데이터베이스의 Share에서 해당 통합을 초대해 읽기 권한을 부여합니다.
- 데이터베이스 ID와 통합 토큰을 기록해둡니다.
Status = Published 그리고 Visibility = Public 조건만 GitHub로 내보내도록 하면 내부 문서가 외부로 나갈 위험을 줄일 수 있습니다.GitHub 준비: 저장소 레이아웃과 배포 대상
문서가 들어갈 저장소와 배포 방식을 결정합니다. 가장 단순한 선택지는 GitHub Pages + 정적 문서 프레임워크입니다.
추천 레이아웃
.
├─ .github/workflows/
│ └─ docs-sync.yml
├─ docs/
│ ├─ guides/
│ ├─ reference/
│ └─ assets/ # 이미지/첨부
├─ docusaurus.config.js (또는 mkdocs.yml / next.config.js)
└─ package.json
배포 대상
- Docusaurus: 개발자 문서·버저닝에 강함
- MkDocs + Material: 심플하고 빠른 레퍼런스
- Next.js: 커스텀 요구사항이 많을 때 유연
어떤 프레임워크든 docs/ 아래 Markdown을 읽어 사이트를 생성하도록 설정하면 됩니다.
데이터 흐름 패턴: 단방향 vs. 양방향
| 패턴 | 장점 | 주의점 | 언제? |
| 단방향 (Notion → GitHub) | 단순, 충돌 적음, 운영 용이 | GitHub에서 직접 수정 시 덮어쓸 수 있음 | 소스 오브 트루스가 Notion일 때 |
| 양방향 (GitHub ↔ Notion) | 어디서나 수정 가능 | 동기화 충돌, 난도↑ | 특수 요구·에디터 다양성이 중요할 때 |
구현 A안(가장 단순): 스케줄 동기화
정해진 주기마다 GitHub Actions가 Notion을 조회하고 변경분을 Markdown으로 생성해 커밋합니다. 세팅이 간단하고 운영이 쉽습니다.
1) GitHub Secrets 설정
NOTION_TOKEN: Notion 통합 토큰NOTION_DATABASE_ID: 대상 데이터베이스 IDGIT_COMMITTER_NAME,GIT_COMMITTER_EMAIL: 커밋 메타
2) Actions 워크플로우 예시
# .github/workflows/docs-sync.yml
# ...
name: Docs Sync (Notion → GitHub)
on:
schedule:
- cron: "15 * * * *" # 매시간 15분에 동기화
workflow_dispatch: {} # 필요시 수동 실행
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
# ...
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install Deps
run: npm ci
- name: Run Sync
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }}
run: node scripts/notion-sync.js
- name: Commit & Push
env:
GIT_AUTHOR_NAME: ${{ secrets.GIT_COMMITTER_NAME }}
GIT_AUTHOR_EMAIL: ${{ secrets.GIT_COMMITTER_EMAIL }}
GIT_COMMITTER_NAME: ${{ secrets.GIT_COMMITTER_NAME }}
GIT_COMMITTER_EMAIL: ${{ secrets.GIT_COMMITTER_EMAIL }}
run: |
if [[ -n "$(git status --porcelain)" ]]; then
git add -A
git commit -m "docs: sync from Notion"
git push
else
echo "No changes"
fi
# ...
3) Node 스크립트 기본 골격
Notion SDK로 페이지를 조회하고 Markdown으로 변환한 뒤 docs/ 디렉터리에 저장합니다.
// scripts/notion-sync.js
import fs from "node:fs";
import path from "node:path";
import {Client} from "@notionhq/client";
// 예: notion-to-md, 혹은 직접 변환 로직 작성
import {NotionToMarkdown} from "notion-to-md";
const notion = new Client({auth: process.env.NOTION_TOKEN});
const n2m = new NotionToMarkdown({notionClient: notion});
const DB_ID = process.env.NOTION_DATABASE_ID;
const DOCS_DIR = path.join(process.cwd(), "docs");
function toSlug(value) {
return (value || "").trim().toLowerCase()
.replace(/[^a-z0-9/-_ ]/g, "")
.replace(/\s+/g, "-");
}
function frontmatter({title, slug, tags = [], category, updated, visibility}) {
const lines = [
"---",
`title: "${title.replace(/"/g, '\\"')}"`,
`slug: ${slug}`,
`tags: [${tags.map(t => `"${t}"`).join(", ")}]`,
`category: ${category || "Guide"}`,
`updated: ${updated || new Date().toISOString()}`,
`visibility: ${visibility || "Public"}`,
"---",
""
];
return lines.join("\n");
}
async function fetchPages() {
const pages = [];
let cursor = undefined;
while (true) {
const res = await notion.databases.query({
database_id: DB_ID,
start_cursor: cursor,
filter: {
and: [
{property: "Status", select: {equals: "Published"}},
{property: "Visibility", select: {equals: "Public"}}
]
},
sorts: [{property: "Updated", direction: "descending"}]
});
pages.push(...res.results);
if (!res.has_more) break;
cursor = res.next_cursor;
}
return pages;
}
async function pageToMarkdown(page) {
const mdBlocks = await n2m.pageToMarkdown(page.id);
const md = n2m.toMarkdownString(mdBlocks);
return md.parent || md;
}
function prop(page, key) {
const p = page.properties[key];
if (!p) return null;
if (p.type === "title") return p.title?.[0]?.plain_text || "";
if (p.type === "select") return p.select?.name || "";
if (p.type === "multi_select") return p.multi_select?.map(x => x.name) || [];
if (p.type === "date") return p.date?.start || "";
if (p.type === "people") return p.people?.map(x => x.name) || [];
if (p.type === "rich_text") return p.rich_text?.[0]?.plain_text || "";
return null;
}
async function main() {
if (!fs.existsSync(DOCS_DIR)) fs.mkdirSync(DOCS_DIR, {recursive: true});
const pages = await fetchPages();
for (const page of pages) {
const title = prop(page, "Title") || "Untitled";
const rawSlug = prop(page, "Slug") || toSlug(title);
const category = prop(page, "Category") || "Guide";
const tags = prop(page, "Tags") || [];
const visibility = prop(page, "Visibility") || "Public";
const updated = prop(page, "Updated") || new Date().toISOString();
// 서브폴더 지원 (예: guides/abc)
const relPath = rawSlug.endsWith(".md") ? rawSlug : `${rawSlug}.md`;
const outPath = path.join(DOCS_DIR, relPath);
const outDir = path.dirname(outPath);
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, {recursive: true});
const body = await pageToMarkdown(page);
const fm = frontmatter({title, slug: `/${rawSlug}`, tags, category, updated, visibility});
fs.writeFileSync(outPath, fm + body + "\n");
console.log("Wrote:", outPath);
}
}
main().catch(err => {
console.error(err);
process.exit(1);
});
구현 B안(변경 감지): 수동 트리거/PR 기반
Notion 변경을 즉시 배포하고 싶거나, 배포 전 리뷰를 강제하고 싶다면 PR 기반이 좋습니다. 워크플로우는 같지만 결과물을 새 브랜치에 커밋하고 자동으로 PR을 열도록 합니다.
# Commit & PR 단계만 변경
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
commit-message: "docs: sync from Notion"
title: "docs(notion): sync"
branch: "chore/docs-sync"
delete-branch: true
팀은 PR에서 미리보기 링크(예: Vercel Preview, Pages preview)를 보고 승인/수정 후 병합하면 됩니다.
콘텐츠 변환: Markdown, 프론트매터, 이미지
Markdown 변환 체크리스트
- 헤딩 레벨: Notion의 헤딩1/2/3 →
#,##,### - 리스트/체크박스: GitHub 호환 마크다운으로
- 코드 블록: 언어 태그 유지 (예:
```ts) - 경고/정보 콜아웃: 프론트엔드 테마의 Alert 구성요소로 매핑
- 테이블: 파이프 테이블 생성 또는 MDX 컴포넌트 사용
- 내부 링크: Notion 페이지 링크 → slug 기반 상대 경로로 치환
프론트매터 규칙(예시)
| 키 | 값 타입 | 용도 |
| title | string | 페이지 제목 |
| slug | string | 고정 URL 경로 |
| tags | string[] | 관련 키워드 필터링 |
| category | string | 사이드바 그룹 |
| updated | ISO string | “최종 업데이트” 표기 |
| visibility | Public/Internal | 내부/외부 구분 |
이미지와 자산
- 다운로드 후
docs/assets/에 저장하고 상대경로로 참조합니다. - 파일명은 슬러그 기반 + 해시로 충돌을 방지합니다.
- 너비/품질을 자동 리사이즈해 페이지 속도를 개선하세요.
간단한 이미지 처리 스니펫
// 예시: URL → 파일 다운로드 후 상대 경로 치환
import { createWriteStream } from "node:fs";
import fetch from "node-fetch";
async function downloadAsset(url, outPath) {
const res = await fetch(url);
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
await new Promise((resolve, reject) => {
const file = createWriteStream(outPath);
res.body.pipe(file);
res.body.on("error", reject);
file.on("finish", resolve);
});
return outPath;
}
품질 게이트: 링크·맞춤법·스타일 체크
코드처럼 문서에도 CI 품질 게이트를 걸어야 일관된 결과가 나옵니다.
- 링크 검사: 내부/외부 링크 죽은 링크 감지
- 맞춤법/스타일: 린터 혹은 사전 기반 검사
- 파일 구조: 금지된 경로나 파일명 패턴 차단
- 프론트매터 유효성: 필수 키 누락 방지
샘플 스크립트: 프론트매터 유효성
// scripts/validate-frontmatter.js
import fs from "node:fs";
import path from "node:path";
import matter from "gray-matter";
const DOCS_DIR = path.join(process.cwd(), "docs");
const required = ["title", "slug", "updated"];
function walk(dir) {
return fs.readdirSync(dir, {withFileTypes: true}).flatMap((ent) => {
const p = path.join(dir, ent.name);
return ent.isDirectory() ? walk(p) : p.endsWith(".md") || p.endsWith(".mdx") ? [p] : [];
});
}
let ok = true;
for (const file of walk(DOCS_DIR)) {
const raw = fs.readFileSync(file, "utf8");
const {data} = matter(raw);
for (const key of required) {
if (!(key in data)) {
console.error(`Missing "${key}" in ${file}`);
ok = false;
}
}
}
if (!ok) process.exit(1);
워크플로우에 품질 단계 추가
- name: Validate Docs
run: node scripts/validate-frontmatter.js
* name: Check Links
run: npx markdown-link-check -q -c .mlc.json "docs/**/*.md"
운영 팁: 권한, 보안, 성능, 로그
보안
- Notion 토큰은 Repository secrets로만 보관
- 공개 저장소라면 내부 문서 필터를 강하게 설정
- PR 미리보기에서 비공개 자산 노출 금지
성능
- 페이지 수백 개면 페이징/캐시 적용
- 변경분만 커밋하도록 해 Git 기록을 깔끔하게
- 이미지는 사이즈 제한과 중복 해시 관리
가시성
- 동기화 로그를 아카이브해 회귀 분석
- 성공/실패를 채널로 통지(Slack, Teams)
권한
- 승인된 사람만 Published로 바꿀 수 있게 권한 분리
- PR 리뷰 규칙과 CODEOWNERS로 책임 명확화
문제 해결: 자주 발생하는 오류와 대응
| 증상 | 원인 | 대응 |
| 동기화는 성공했는데 페이지가 비어 있음 | 변환 로직이 지원하지 않는 블록 타입 | 해당 블록을 안전한 Markdown 대체로 치환하거나 MDX 컴포넌트 사용 |
| 이미지가 깨짐 | 만료 URL 참조, 권한 문제 | 다운로드 후 로컬 자산으로 저장, 상대경로 사용 |
| 내부 문서가 외부에 노출 | 필터 누락 | Status/Visibility 2중 필터로 보호 |
| 배포는 되었는데 스타일이 일관적이지 않음 | 프론트매터/헤딩 규칙 미준수 | 린터와 PR 체크로 강제 |
마이그레이션 & 확장: Docusaurus/MkDocs/Next
이미 운영 중인 사이트가 있다면 변환 스크립트에서 출력 포맷만 바꾸면 됩니다.
- Docusaurus: 사이드바 자동 생성 규칙에 맞춰
sidebar_position등 추가 - MkDocs:
mkdocs.yml의nav업데이트 자동화 - Next.js:
getStaticProps로 Markdown을 읽어 페이지 생성
장기적으로는 다국어(i18n) 지원, 버전 문서, 검색 색인(예: Algolia)까지 파이프라인에 통합할 수 있습니다.
결론 요약 및 다음 단계
- Notion을 작성의 단일 소스로 두고, GitHub로 자동 동기화하면 문서 품질과 배포 속도가 모두 올라갑니다.
- 가장 단순한 A안(스케줄 동기화)으로 시작하고, 팀이 익숙해지면 PR 기반 검토를 추가하세요.
- 프론트매터 규칙·이미지 처리·품질 게이트를 도입하면 확장성과 유지보수가 좋아집니다.
다음 단계 체크리스트
- Notion DB 스키마 만들기(Title, Slug, Status, Visibility 등)
- Notion 통합 토큰 발급 & 데이터베이스 공유
- GitHub 저장소와 문서 프레임워크 선택
- Actions 워크플로우와 변환 스크립트 배치
- 품질 게이트와 PR 규칙 설정
FAQ
Q1. Notion에서 바로 배포를 누를 수 있나요?
A. 일반적으로는 GitHub Actions가 중간에서 변환·검증·배포를 담당합니다. Notion은 작성 도구, GitHub는 자동화와 배포 도구로 역할을 분리하는 편이 운영상 안정적입니다.
Q2. 내부 전용 문서와 외부 공개 문서를 동시에 관리할 수 있을까요?
A. 가능합니다. Visibility 필드를 기준으로 Public만 외부 저장소로 내보내고, Internal은 별도 저장소/브랜치로 동기화하거나 아예 동기화 대상에서 제외하세요.
Q3. 이미지나 첨부 파일 용량이 큰데 어떻게 처리하나요?
A. 동기화 시점을 기준으로 리사이즈/압축을 적용하고, 파일명을 슬러그+해시로 표준화하세요. 장기적으로는 CDN이나 저장 버킷을 연동해 성능과 비용을 최적화할 수 있습니다.
'Backend > Study' 카테고리의 다른 글
| [Tip] 백엔드 개발자 로드맵 2025 (최신 기술 트렌드 포함) (0) | 2025.11.05 |
|---|---|
| [Tip] ChatGPT를 활용한 개발 업무 자동화 사례 (0) | 2025.11.04 |
| [DevOps] Kubernetes에서 ConfigMap과 Secret 관리하기 (0) | 2025.10.31 |
| [DevOps] Kubernetes 초보자 가이드 (백엔드 개발자를 위한 입문) (0) | 2025.10.30 |
| [DevOps] GitLab CI/CD vs GitHub Actions 비교 (0) | 2025.10.29 |