> ## Content Index
> Fetch the complete content index at: https://blog.sorune.org/llms.txt
> Use this file to discover other available public pages before exploring further.

# Ghost를 편집기로 쓰되 원본으로 두지 않은 이유 — Git-first Publishing Factory
- URL: https://blog.sorune.org/git-first-ghost-publishing-factory/
- Published: 2026-09-11T15:23:30.000Z
- Updated: 2026-09-15T01:12:05.000Z
- Description: Ghost는 훌륭한 CMS지만, 글의 원본과 발행 권한까지 CMS 하나에 묶을 필요는 없다. Git을 canonical source로 두고 Ghost는 Draft delivery surface로 사용한 publishing pipeline의 경계와 이유를 정리한다.
- Author: Sorune — Engineering, Photography & Lab Notes
- Tags: Engineering, Ghost, Publishing, Git, CI/CD, Content Pipeline

개인 기술 블로그를 다시 만들면서 처음에는 아주 단순하게 생각했다.

Ghost가 이미 글쓰기 UI, 이미지 관리, 태그, 발행 기능을 모두 제공하니 그냥 Ghost에서 글을 쓰면 되는 것 아닌가?

기능만 보면 맞는 말이다. 하지만 내가 원한 것은 단순한 CMS가 아니라, 개발 과정에서 이미 쌓이고 있는 문서와 evidence를 **검토 가능한 공개 글로 바꾸는 과정**이었다. 그 순간부터 글을 어디에서 작성하느냐보다 더 중요한 질문이 생겼다.

> 무엇이 이 글의 원본인가?

Ghost의 데이터베이스를 원본으로 두면 편집은 편하다. 반면 Git에 이미 존재하는 설계 기록, 실험 결과, 프로젝트 문서와 공개 글 사이의 provenance를 추적하기는 어려워진다. 반대로 Git을 원본으로 두면 diff, review, history, source reference를 그대로 사용할 수 있다.

그래서 둘 중 하나를 버리지 않고 역할을 나눴다.

```text
Git
= canonical article source

Ghost
= editing/review/delivery surface

Public website
= publication surface

```

## 콘텐츠와 발행 권한을 분리한다

가장 먼저 정한 원칙은 단순했다.

```text
ARTICLE SOURCE
!=
PUBLICATION AUTHORITY

```

Markdown 파일이 Git에 존재한다고 해서 인터넷에 공개되어야 하는 것은 아니다. AI가 글을 만들었다고 해서 공개되어도 되는 것도 아니다. CI가 전부 통과했다고 해서 발행 권한을 얻는 것도 아니다.

그래서 글의 lifecycle과 실제 공개 상태를 분리했다.

```mermaid
flowchart LR
    E[Project / History Evidence] --> A[Git Article]
    A --> V[Schema + Content Validation]
    V -->|PASS| D[Ghost Draft]
    V -->|FAIL| X[Stop]
    D --> R[Human Review]
    R -->|Revise| A
    R -->|Approve| P[Ghost Publish]
    P --> W[Public Blog]

    style X stroke-dasharray: 4 4

```

이 흐름에서 자동화가 갈 수 있는 마지막 지점은 `Ghost Draft`다.

그 이후는 사람이 결정한다.

## 왜 Git이 canonical source인가

블로그 글 대부분은 아무것도 없는 편집기에서 갑자기 만들어지지 않는다.

예를 들어 런타임 마이그레이션 글이라면 실제 benchmark 결과가 있고, 아키텍처 글이라면 결정 문서나 roadmap이 있다. 어떤 실패를 정리한 글이라면 당시의 incident record나 review가 있다.

즉 공개 글보다 먼저 존재하는 재료가 있다.

```text
implementation
experiment
review
history
architecture decision
        ↓
public narrative

```

Git을 원본으로 두면 공개 글에서도 이 연결을 유지하기 쉽다.

각 글의 frontmatter에는 제목이나 태그뿐 아니라 해당 글이 어떤 evidence를 기반으로 작성됐는지 `sources`를 남길 수 있다. 이것은 독자에게 내부 저장소를 그대로 노출하기 위한 기능이 아니다. 나중에 글의 문장을 수정하거나 사실을 재검증할 때 **어디에서 출발했는지를 잃어버리지 않기 위한 provenance**다.

또 하나의 장점은 변경 자체가 기록된다는 점이다.

Ghost editor에서 문장을 고치면 최종 결과는 남지만 왜 바뀌었는지는 별도의 운영 규칙이 필요하다. Git에서는 diff가 기본 기능이다.

```text
before
→ change
→ review
→ after

```

콘텐츠도 코드와 완전히 같다고 생각하지는 않는다. 하지만 기술 글에서는 이 변경 이력이 꽤 유용하다.

## 그렇다고 Ghost를 단순 렌더러로 만들지는 않았다

Git-first 구조라고 해서 Ghost의 장점을 버릴 필요는 없다.

Ghost는 여전히 다음을 담당한다.

- 공개 사이트의 post model
- slug 기반 URL
- tag/archive
- image/media delivery
- draft preview
- Admin UI에서의 최종 검토
- 실제 Publish 동작

즉 Git이 글의 의미와 이력을 소유하고, Ghost는 CMS로서 publication surface를 소유한다.

```text
Git owns
what the article says

Ghost owns
how the article lives as a CMS post

```

이 구분 덕분에 Ghost를 나중에 업그레이드하거나 theme를 교체해도 Markdown 원본은 영향을 덜 받는다.

## stable slug가 두 세계를 연결한다

Git 파일과 Ghost post를 연결하기 위해 가장 단순한 identity로 slug를 사용했다.

예를 들어:

```text
slug: git-first-ghost-publishing-factory

```

같은 slug의 Ghost post가 없으면 새 Draft를 만든다. 이미 Draft가 있으면 같은 post를 update한다.

중요한 것은 매번 새 글을 만드는 방식이 아니라는 점이다.

```text
first delivery
→ create draft

next delivery
→ update same draft

```

그리고 같은 slug의 post가 둘 이상 발견되면 자동으로 하나를 고르지 않고 실패하게 했다.

자동화가 identity 충돌을 조용히 덮어버리면 편해 보이지만, 그 순간부터 어떤 글이 canonical delivery target인지 불명확해진다.

## Published post는 자동으로 덮어쓰지 않는다

또 하나의 경계는 이미 공개된 글이다.

Git의 Draft를 수정했다고 해서 이미 Published 상태인 Ghost post를 자동 수정하지 않는다.

```text
Git change
!= automatic public mutation

```

공개된 글은 독자가 실제로 읽고 있는 상태다. 여기부터는 단순한 Draft synchronization이 아니라 public mutation이다.

그래서 publisher는 Draft create/update까지만 허용하고, Published post를 만나면 자동 변경을 중단한다.

덕분에 자동화는 편의 기능이지만 publication authority가 되지 않는다.

## 수동 버튼도 결국 없앴다

초기 acceptance에서는 한 글씩 workflow를 수동 실행했다.

이 방식은 경계를 확인하기에는 좋았다. create, same-slug update, duplicate protection을 하나씩 검증할 수 있었기 때문이다.

하지만 검증이 끝난 뒤에도 매번 Actions 화면에서 글 경로를 입력하는 것은 단순한 반복 작업이었다.

그래서 현재 흐름은 `main`에 Draft/Review article이 변경되면 해당 파일을 자동으로 찾아 Ghost Draft까지 전달한다.

```text
commit article
     ↓
validation
     ↓
changed article detection
     ↓
Ghost draft create/update
     ↓
duplicate check
     ↓
stop

```

여기서 중요한 것은 자동화 범위를 넓힌 것이 아니라 **이미 검증된 범위 안에서 사람의 불필요한 클릭만 제거했다는 점**이다.

Publish는 여전히 자동화하지 않았다.

## 실패는 가능한 한 공개 전에 발생해야 한다

이 파이프라인에서 CI의 역할은 글을 잘 썼는지 판단하는 것이 아니다.

CI가 확인할 수 있는 것은 훨씬 제한적이다.

예를 들면:

- frontmatter가 schema를 만족하는가
- slug가 올바른가
- 필수 metadata가 있는가
- source lifecycle과 status가 모순되지 않는가
- publisher가 build/test를 통과하는가
- 명백한 credential/private-path 형태가 섞이지 않았는가

이것들은 문장 품질이나 사실의 모든 의미를 검증하지 못한다.

하지만 기계가 확실히 잡을 수 있는 오류를 사람에게 넘길 필요도 없다.

```text
machine-verifiable problems
→ CI

meaning / tone / publication decision
→ Human Review

```

자동화와 사람의 역할을 경쟁시키기보다 서로 다른 종류의 오류를 맡기는 구조다.

## 이 구조가 필요한 규모인가

개인 블로그 하나에 이 정도가 과한 것처럼 보일 수도 있다.

실제로 글을 몇 개만 직접 쓸 거라면 Ghost editor만 사용하는 것이 더 빠르다.

하지만 내 경우에는 이미 프로젝트 저장소와 History에 기술적인 기록이 계속 쌓이고 있고, 이 중 일부를 나중에 공개 가능한 형태로 다시 정리하고 싶었다. 그러면 반복되는 변환 과정 자체가 생긴다.

```text
internal evidence
→ public-safe narrative
→ review
→ publication

```

Publishing Factory는 거대한 CMS를 새로 만들기 위한 시스템이 아니다. 이 반복 과정에서 **원본, 자동화, 공개 권한의 경계를 잃지 않기 위한 작은 파이프라인**에 가깝다.

## 가장 중요한 것은 자동화가 아니라 멈추는 위치였다

처음에는 무엇을 자동화할지가 핵심이라고 생각하기 쉽다.

하지만 실제로 구조를 정리하면서 더 중요했던 질문은 반대였다.

> 자동화는 어디에서 반드시 멈춰야 하는가?

현재 답은 명확하다.

```text
Git article
→ automatic

Validation
→ automatic

Ghost Draft
→ automatic

Public Publish
→ Human

```

자동화를 많이 하는 것보다, 자동화가 권한을 넘어가지 않는 위치를 명확히 하는 편이 더 중요했다.

그 덕분에 앞으로 History나 프로젝트 문서에서 새로운 글을 만들더라도 같은 흐름을 재사용할 수 있다. 글 작성 방식이나 theme가 바뀌어도 source와 publication authority의 경계는 그대로 유지할 수 있다.

Ghost는 계속 CMS다. Git은 계속 Git이다.

둘을 억지로 하나로 만들지 않고, 각자가 잘하는 부분 사이에 작은 계약을 둔 것이 이 구조의 핵심이다.