기본적인 글 렌더링 구조를 만들고 나니 다음 고민이 생겼다.

글을 쓰는 저장소와 블로그 앱 저장소를 분리하면, 글을 저장하는 시점과 실제로 공개하는 시점을 더 명확하게 나눌 수 있다.

처음에는 단순하게 blog 저장소 안에 글을 두려고 했다.

같은 저장소에 글을 두는 구조
blog└── content/posts    └── my-post.mdx

이 방식은 쉽다. 글을 수정하고 main에 push하면 바로 배포된다.

하지만 실제로 글을 쓰는 흐름을 생각해보니 아쉬운 점이 있었다.

  • 글 초안을 자유롭게 쌓아두고 싶었다.
  • 아직 정리 중인 글이 실수로 공개되는 일을 피하고 싶었다.
  • 앱 코드 수정과 글 수정 커밋을 분리하고 싶었다.
  • 글을 저장하는 시점과 공개하는 시점을 나누고 싶었다.

그래서 글 저장소와 블로그 앱 저장소를 분리하기로 했다.

저장소를 두 개로 나누었다

현재 구조는 이렇게 나뉜다.

blog는 사용자가 보는 블로그 앱이다.

blog-posts는 글을 쓰고 보관하는 private 저장소다.

submodule pointer가 공개 기준이다

이 구조를 만들기 위해 Git submodule을 사용했다.

Git submodule은 한 저장소 안에서 다른 저장소를 특정 커밋으로 연결하는 기능이다.

이 블로그에서는 blog 저장소 안에 content-source/posts라는 submodule이 있다.

submodule 연결 구조
blog└── content-source/posts -> blog-posts의 특정 commit

blog 저장소는 blog-posts의 최신 상태를 자동으로 따라가지 않는다.

대신 이렇게 기록한다.

이번 배포에서 사용할 blog-posts 커밋은 무엇인가?

이 기록을 submodule pointer라고 보면 된다.

pointer는 말 그대로 "어디를 가리키는지"를 나타내는 값이다. blog 저장소에 기록된 pointer가 바뀌어야 실제 배포 대상도 바뀐다.

다만 pointer가 가리킨다고 해서 그 안의 모든 글이 무조건 공개되는 것은 아니다.

블로그 앱은 pointer가 가리키는 blog-posts 커밋을 읽은 뒤, 각 MDX 파일의 frontmatter를 확인한다.

draft: true

이 값이면 비공개 글로 보고 목록, 상세 페이지, RSS, sitemap에서 제외한다.

draft: false

이 값이면 공개 가능한 글로 본다.

즉 공개 여부는 두 단계로 결정된다.

  1. 1blog의 submodule pointer가 이번 배포에 사용할 blog-posts 커밋을 정한다.
  2. 2그 커밋 안에서 각 글의 draft 값이 실제 노출 여부를 정한다.

그래서 blog-posts에서는 글을 자유롭게 새로 작성하고, 수정하고, 삭제하고, draft: true로 비공개 글도 둘 수 있다.

production에는 blog가 선택한 커밋 중에서도 draft: false인 글만 보인다.

전체 구조

전체 흐름을 단순하게 그리면 아래와 같다.

핵심은 이렇다.

역할 정리

  • blog-posts는 글을 쓰고 보관하는 공간이다.
  • blog는 어떤 글 저장소 커밋을 배포에 사용할지 결정하는 공간이다.
  • draft는 선택된 커밋 안에서 글을 실제로 보여줄지 결정하는 값이다.

실제 동작 방식

글 하나를 새로 공개한다고 생각해보자.

먼저 blog-posts에서 글을 작성한다.

---
title: 새 글
draft: false
---

작성한 글을 blog-posts에 커밋하고 push한다.

blog-posts commit A

하지만 아직 production은 바뀌지 않는다.

이유는 단순하다.

blog 저장소가 아직 commit A를 가리키지 않기 때문이다.

이제 blog 저장소에서 content-source/posts를 commit A로 갱신한다.

pointer 갱신 후
blog└── content-source/posts -> blog-posts commit A

그리고 이 pointer 변경을 blog 저장소에 커밋한다.

blog commit
  - content-source/posts pointer 변경

이 커밋은 글 파일 전체를 복사하는 커밋이 아니다.

blog 저장소가 사용할 blog-posts 커밋 SHA를 바꾸는 커밋이다.

정리하면 게시 흐름은 이렇게 된다.

  1. 1

    blog-posts에서 글을 작성한다.

  2. 2

    blog-posts에 글 커밋을 push한다.

  3. 3

    blog에서 content-source/posts pointer를 갱신한다.

  4. 4

    blog에 pointer 변경 커밋을 만든다.

  5. 5

    blog main에 push한다.

  6. 6

    GitHub Actions가 기록된 글 커밋으로 배포한다.

이 구조에서는 blog-posts의 최신 main이 자동으로 배포되지 않는다.

항상 blog 저장소에 기록된 submodule pointer가 기준이고, 실제 화면에 보이는 글은 그 안에서 draft: false인 글이다.

왜 이렇게 나누었나

이 구조를 선택한 이유는 세 가지다.

1. 초안을 자유롭게 관리하기 위해

글은 한 번에 완성되지 않는다.

제목을 바꾸고, 문장을 덜어내고, 예시 코드를 다시 정리하는 과정이 필요하다.

blog-posts를 private 저장소로 분리하면 이런 작업을 부담 없이 할 수 있다. 새 글을 만들고, 기존 글을 수정하고, 더 이상 필요 없는 글을 삭제하고, 아직 공개하고 싶지 않은 글은 draft: true로 남겨둘 수 있다.

2. 공개 시점을 명확하게 하기 위해

blog-posts에 push하는 것은 글을 저장하는 일이다.

blog의 submodule pointer를 갱신하는 것은 배포에 사용할 글 저장소 버전을 선택하는 일이다.

그리고 각 글의 draft 값은 선택된 버전 안에서 실제로 공개할지 말지를 정하는 값이다.

이 둘을 나누면 기준이 분명해진다.

3. 어떤 글 버전이 배포됐는지 추적하기 위해

blog 저장소의 커밋을 보면 당시 content-source/posts가 어떤 blog-posts 커밋을 가리켰는지 알 수 있다.

즉, 특정 배포에 어떤 글 버전이 포함됐는지 추적하기 쉽다.

같은 저장소에 두는 방식과 비교

같은 저장소에 글을 두는 방식과 지금 구조를 비교하면 이렇다.

작은 블로그에서는 같은 저장소 방식도 충분히 좋은 선택이다.

하지만 나는 공개 단계를 하나 더 두고 싶었다.

글 수정
blog-posts push
blog pointer 갱신
main push
배포

단계는 늘었지만, 이 단계가 공개 승인 역할을 한다.

내 경우에는 이 명시적인 단계가 더 잘 맞았다. 글 저장소에서는 자유롭게 작업하고, 블로그 앱에서는 이번 배포에 사용할 커밋을 고르고, 글 파일 안에서는 draft로 노출 여부를 정할 수 있기 때문이다.

자동 배포는 가볍게만 이해하기

이 구조가 실제로 운영되려면 pointer가 바뀌었을 때 배포가 실행되어야 한다.

나는 이 부분을 GitHub Actions와 Vercel CLI로 자동화했다.

blog main push
  -> GitHub Actions 실행
  -> blog에 기록된 blog-posts 커밋 checkout
  -> Vercel production 배포

이 글에서는 저장소 분리와 공개 기준을 이해하는 데 집중하고, GitHub Actions YAML, GH_PAT 권한, Vercel 프로젝트 ID 같은 세부 설정은 깊게 다루지 않는다.

실제 운영 절차와 설정값은 Wiki에 따로 정리했다.

자동 배포 구현 과정은 이후 글에서 따로 다룰 예정이다.

마무리

이번 구조의 핵심은 단순하다.

blog-posts는 자유롭게 글을 쓰는 공간이다.

blog는 어떤 글 버전을 production에 올릴지 결정하는 공간이다.

submodule pointer는 그 둘 사이에서 "이번 배포에 사용할 글 커밋"을 기록한다.

그리고 draft는 그 커밋 안에서 "이 글을 실제로 보여줄지"를 정한다.

처음에는 submodule이 조금 복잡하게 느껴졌다. 하지만 직접 운영해보니 이 복잡함은 공개 시점을 제어하기 위한 장치에 가까웠다.

덕분에 private 저장소에서는 글을 마음껏 새로 쓰고, 고치고, 지우고, 비공개로 남겨둘 수 있다. production에는 내가 명시적으로 선택한 커밋 안에서 공개 상태인 글만 올라간다.