블로그를 앱이 아니라 플랫폼으로 만든 이유
Next.js 블로그 하나면 끝날 일을 플러그인 플랫폼으로 만들었어요. 과해 보인다는 거 압니다. 그런데 다음 사이트가 이미 줄 서 있었거든요. 그 결정의 기록과 대가입니다.

가족이 쓸 블로그가 필요했어요. 상식적인 답: Next.js로 블로그 하나 뚝딱. 제가 한 것: 블로그가 첫 번째 제품으로 올라가는 플랫폼 만들기.
네, 과해 보인다는 거 압니다. 그런데 이유가 있어요.
왜 그냥 Next.js 블로그를 안 만들었나
다음 사이트가 이미 예정돼 있었어요
가족용 다이어리, 계산기형 사이트, 지도 사이트. 블로그를 앱으로 만들면 다이어리를 만들 때 저장, 작성/수정/archive 수명주기, 목록/상세 라우팅, 게시 파이프라인을 전부 다시 짜게 돼요. 이전 프로젝트에서 사이트마다 같은 코드를 반복해서 짰고, 그 반복이 싫었습니다.
앱으로 만들면 두 번째부터 무엇을 다시 짜나
"다시 짜게 된다"를 구체적으로 세어 봤어요. 다이어리를 만든다고 가정하고요.
| 기능 | 블로그에서 만든 것 | 다이어리에서 | 사이트마다 다른가 |
|---|---|---|---|
| 데이터 저장 | 리비전 · 낙관적 동시성 | 같은 것 다시 | 아니오 |
| 작성 → 보관 → 삭제 | 상태 전이와 규칙 | 같은 것 다시 | 아니오 |
| 목록 · 상세 라우팅 | 슬러그 · 페이지네이션 | 같은 것 다시 | 아니오 |
| 초안과 공개본 분리 | 릴리스 · 롤백 | 같은 것 다시 | 아니오 |
| 필드 정의 | 제목 · 본문 · 태그 | 날짜 · 기분 · 사진 | 예 |
| 화면 표현 | 글 카드 | 달력 격자 | 예 |
마지막 두 줄만 다릅니다. 위 네 줄은 어떤 사이트든 똑같아요. 그런데 앱으로 만들면 그 네 줄이 앱 안에 녹아 있어서, 두 번째 사이트에서 꺼낼 수가 없습니다.
이전 프로젝트에서 사이트마다 같은 코드를 복사하며 이미 겪었어요. 복사한 코드는 각자 조금씩 달라지고, 어느 순간 "고치려면 세 군데를 고쳐야 하는" 상태가 됩니다.
그래서 질문을 바꿨어요. "블로그를 어떻게 만들까"가 아니라 "무엇이 반복되고 무엇이 다른가"로요.
반복되는 것과 다른 것을 갈랐습니다
데이터 저장, CRUD 수명주기, 라우팅, 공개 렌더링, Draft/Release 게시는 어떤 사이트든 똑같아요 — 플랫폼 코어가 가집니다. 블로그 글의 필드, 다이어리 엔트리의 의미, 계산기의 계산 엔진은 사이트마다 달라요 — 플러그인이 가집니다. "CRUD lifecycle은 플랫폼 책임, CRUD 대상과 도메인 UX는 플러그인 책임" — 이게 이 프로젝트의 테제예요.
Host가 플러그인을 직접 아는 구조를 왜 버렸나
제일 쉬운 길은 Host가 플러그인을 import하고 분기하는 거예요. 실제로 처음엔 그렇게 시작했고요.
// 기각한 구조
import { blogPlugin } from "@arcac/plugin-blog";
import { diaryPlugin } from "@arcac/plugin-diary";
function resolve(siteType) {
if (siteType === "blog") return blogPlugin;
if (siteType === "diary") return diaryPlugin;
throw new Error("알 수 없는 사이트 유형");
}
// 플러그인이 늘 때마다 이 파일이 자랍니다.
// 그리고 이 파일은 코어에 있습니다.| 직접 import | 매니페스트 조합 |
|---|---|
| 플러그인이 늘면 코어가 자람 | 코어는 그대로 |
| 코어가 도메인 단어를 앎 | 코어에 blog 라는 단어가 없음 |
| 안 쓰는 플러그인도 번들에 들어옴 | 설치된 것만 로드 |
| 새 사이트 유형 = 코어 수정 | 새 사이트 유형 = 매니페스트 추가 |
두 번째 줄이 이 설계에서 스스로에게 건 가장 강한 규율이에요. 플랫폼 패키지 어디에도 `blog`라는 도메인 분기를 넣지 않는다.
그리고 이건 선언이 아니라 검사입니다. 코어 패키지 소스에서 도메인 이름이 등장하면 빌드가 막혀요. 문서로만 두면 급한 날에 한 줄이 들어가고, 그 한 줄이 다음 줄을 부릅니다.
Manifest로 조합하고, Composer는 도메인을 모른다
플러그인을 나눴다고 끝이 아니에요. Host가 플러그인을 직접 import하고 if (siteType === 'blog') 식으로 분기하면, 플러그인이 늘 때마다 Host가 같이 비대해지거든요. 이 방식을 기각하고(ADR 0001), 플러그인은 정적 Manifest만 선언하게 했습니다.
export interface PluginManifest {
readonly schemaVersion: 1;
readonly id: string;
readonly version: string;
readonly engine: string;
readonly entrypoints: Readonly<Record<string, PluginEntrypoint>>;
readonly contributions: readonly PluginContribution[];
readonly dependencies: readonly PluginDependency[];
readonly permissions: readonly string[];
}Composer는 blog라는 단어를 모릅니다
PlatformComposer가 이 Manifest들로 의존성 그래프를 만들어 Extension Point에 연결해요. 여기서 스스로에게 건 가장 강한 규율 — 플랫폼 패키지에 if blog 같은 도메인 분기를 넣지 않고, platform.blog.post/v1 같은 extension point 이름도 금지. 도메인 이름은 contribution id나 ContentType id(blog.post)에만 허용돼요. 판정 기준도 문서에 못 박아 뒀습니다. 두 번째 제품을 만들 때 플랫폼 패키지가 많이 바뀌면 설계 실패다.

Registry와 Repository는 다릅니다
Registry는 "무엇을 만들고 확장할 수 있는가", Repository는 "사용자가 실제로 무엇을 만들었는가"예요. Post Type, Renderer, Theme의 정의는 Registry에 살고, 사용자가 쓴 실제 글과 Draft는 Repository에 살아요. 이 구분 덕에 "블로그 글 타입을 안다"와 "블로그 글을 저장한다"가 다른 레이어라는 게 코드에서 강제됩니다. 게시 시점에는 Registry에 Overlay를 얹어 freeze한 불변 RuntimeSnapshot을 만들고(ADR 0002), Release 포인터를 원자적으로 교체해요. 실행 중인 사이트가 다른 테넌트의 플러그인 변경에 오염되지 않는 건 이 불변성 덕입니다.
Registry와 Repository를 가른 것도 같은 맥락이에요. 헷갈리기 쉬운데 성격이 완전히 다릅니다.
| Registry | Repository | |
|---|---|---|
| 답하는 질문 | 무엇을 만들 수 있는가 | 사용자가 무엇을 만들었는가 |
| 담기는 것 | 포스트 타입 · 렌더러 · 테마 정의 | 실제 글 · 초안 · 릴리스 |
| 누가 채우나 | 설치된 플러그인 | 사용자 |
| 언제 바뀌나 | 설치·업데이트 시점 | 수시로 |
| 없으면 | 만들 수 있는 게 없음 | 만든 게 없음 |
이걸 안 가르면 "사용자가 만든 것"이 "만들 수 있는 것"의 목록에 섞여 들어가요. 그러면 사용자 데이터가 플랫폼 설정이 되고, 플러그인을 지웠을 때 무엇을 지워야 할지 알 수 없게 됩니다.
반대로 갈라 두면 규칙이 단순해져요. 플러그인을 지우면 Registry에서 정의가 사라지고, 사용자가 만든 것은 Repository에 그대로 남습니다. 다시 설치하면 다시 보이고요.
대가는 있었어요
이 선택은 공짜가 아니었어요. Host가 직접 import했으면 필요 없었을 것들 — Manifest 스키마, Catalog Generator, Artifact 검증 체계 — 를 전부 만들어야 했죠. 블로그 글 하나 화면에 띄우기까지의 거리가 일반 앱보다 훨씬 멀었습니다.
("이 추상화가 정말 필요한가" 스스로에게 물은 날도 많았어요.)
치른 값을 숫자로
"공짜가 아니었다"를 조금 구체적으로 적어 둘게요.
| 만들어야 했던 것 | 직접 import 였다면 | 왜 필요했나 |
|---|---|---|
| 매니페스트 스키마와 검증 | 불필요 | 조합 대상을 데이터로 표현해야 해서 |
| 카탈로그 생성기 | 불필요 | 손으로 관리하는 중앙 목록을 없애려고 |
| 아티팩트 계약과 무결성 검사 | 불필요 | 조립된 결과가 맞는지 확인하려고 |
| 확장 지점 계약 | 불필요 | 플러그인이 코어를 모르고도 끼어들려고 |
블로그 글 하나를 화면에 띄우기까지 걸린 시간이, 그냥 만들었으면 훨씬 짧았을 거예요. "이 추상화가 정말 필요한가"를 스스로에게 물은 날도 많았고요.
그리고 그 질문에는 아직 완전한 답이 없습니다. 가설이 참인지는 두 번째 제품을 올려 봐야 알거든요.
그리고 증명이 하나 남아 있어요. 이 플랫폼의 가설은 "두 번째 제품은 플러그인만 추가하면 된다"인데, 지금 올라간 제품은 블로그 하나뿐이거든요. 다이어리나 계산기를 실제로 올려봐야 경계가 제대로 그어졌는지 판정할 수 있어요. Composer가 blog라는 단어를 모르는 게 그때 배당금으로 돌아올지 — 그게 이 아키텍처의 성적표가 될 겁니다.
그래도 지금 시점의 중간 결산은 긍정적이에요. 블로그 UX를 수십 커밋에 걸쳐 정련하는 동안 플랫폼 코어는 거의 흔들리지 않았어요. 기능은 플러그인과 contribution에 쌓였고, 코어에는 계약만 남았습니다. 블로그를 만든 게 아니라 "블로그를 서술할 수 있는 언어"를 만들었다는 감각 — 그게 이 우회로에서 얻은 거예요.
(참고로 지금 읽고 계신 이 글도 그 플랫폼 위에서 발행되고 있습니다.)
이 시리즈는 시작과 회고의 이야기예요. 코어를 지탱하는 경계선들은 "플랫폼 코어 설계" 시리즈에서, 그 위에서 블로그라는 제품을 완성해 가는 이야기는 "쓰고, 발행하기" 시리즈부터 이어집니다.


