전체 검색

글, 태그와 카테고리를 검색하고 바로 이동해요

블로그를 앱이 아니라 플랫폼으로 만든 이유

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

2026. 07. 14. 11:45:316분 읽기프로젝트/arcaC/아키텍처
#플랫폼 설계#ADR#플러그인 아키텍처#Manifest#Registry
조회 45
플랫폼 위에 첫 제품으로 꽂히는 블로그 일러스트

가족이 쓸 블로그가 필요했어요. 상식적인 답: 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)에만 허용돼요. 판정 기준도 문서에 못 박아 뒀습니다. 두 번째 제품을 만들 때 플랫폼 패키지가 많이 바뀌면 설계 실패다.

Manifest 조합 파이프라인 다이어그램
(플러그인은 선언하고, 플랫폼이 조합한다 — 이 글의 요지를 그림으로.)

Registry와 Repository는 다릅니다

Registry는 "무엇을 만들고 확장할 수 있는가", Repository는 "사용자가 실제로 무엇을 만들었는가"예요. Post Type, Renderer, Theme의 정의는 Registry에 살고, 사용자가 쓴 실제 글과 Draft는 Repository에 살아요. 이 구분 덕에 "블로그 글 타입을 안다"와 "블로그 글을 저장한다"가 다른 레이어라는 게 코드에서 강제됩니다. 게시 시점에는 Registry에 Overlay를 얹어 freeze한 불변 RuntimeSnapshot을 만들고(ADR 0002), Release 포인터를 원자적으로 교체해요. 실행 중인 사이트가 다른 테넌트의 플러그인 변경에 오염되지 않는 건 이 불변성 덕입니다.

Registry와 Repository를 가른 것도 같은 맥락이에요. 헷갈리기 쉬운데 성격이 완전히 다릅니다.

RegistryRepository
답하는 질문무엇을 만들 수 있는가사용자가 무엇을 만들었는가
담기는 것포스트 타입 · 렌더러 · 테마 정의실제 글 · 초안 · 릴리스
누가 채우나설치된 플러그인사용자
언제 바뀌나설치·업데이트 시점수시로
없으면만들 수 있는 게 없음만든 게 없음

이걸 안 가르면 "사용자가 만든 것"이 "만들 수 있는 것"의 목록에 섞여 들어가요. 그러면 사용자 데이터가 플랫폼 설정이 되고, 플러그인을 지웠을 때 무엇을 지워야 할지 알 수 없게 됩니다.

반대로 갈라 두면 규칙이 단순해져요. 플러그인을 지우면 Registry에서 정의가 사라지고, 사용자가 만든 것은 Repository에 그대로 남습니다. 다시 설치하면 다시 보이고요.

대가는 있었어요

이 선택은 공짜가 아니었어요. Host가 직접 import했으면 필요 없었을 것들 — Manifest 스키마, Catalog Generator, Artifact 검증 체계 — 를 전부 만들어야 했죠. 블로그 글 하나 화면에 띄우기까지의 거리가 일반 앱보다 훨씬 멀었습니다.

("이 추상화가 정말 필요한가" 스스로에게 물은 날도 많았어요.)

치른 값을 숫자로

"공짜가 아니었다"를 조금 구체적으로 적어 둘게요.

만들어야 했던 것직접 import 였다면왜 필요했나
매니페스트 스키마와 검증불필요조합 대상을 데이터로 표현해야 해서
카탈로그 생성기불필요손으로 관리하는 중앙 목록을 없애려고
아티팩트 계약과 무결성 검사불필요조립된 결과가 맞는지 확인하려고
확장 지점 계약불필요플러그인이 코어를 모르고도 끼어들려고

블로그 글 하나를 화면에 띄우기까지 걸린 시간이, 그냥 만들었으면 훨씬 짧았을 거예요. "이 추상화가 정말 필요한가"를 스스로에게 물은 날도 많았고요.

그리고 그 질문에는 아직 완전한 답이 없습니다. 가설이 참인지는 두 번째 제품을 올려 봐야 알거든요.

그리고 증명이 하나 남아 있어요. 이 플랫폼의 가설은 "두 번째 제품은 플러그인만 추가하면 된다"인데, 지금 올라간 제품은 블로그 하나뿐이거든요. 다이어리나 계산기를 실제로 올려봐야 경계가 제대로 그어졌는지 판정할 수 있어요. Composer가 blog라는 단어를 모르는 게 그때 배당금으로 돌아올지 — 그게 이 아키텍처의 성적표가 될 겁니다.


그래도 지금 시점의 중간 결산은 긍정적이에요. 블로그 UX를 수십 커밋에 걸쳐 정련하는 동안 플랫폼 코어는 거의 흔들리지 않았어요. 기능은 플러그인과 contribution에 쌓였고, 코어에는 계약만 남았습니다. 블로그를 만든 게 아니라 "블로그를 서술할 수 있는 언어"를 만들었다는 감각 — 그게 이 우회로에서 얻은 거예요.

(참고로 지금 읽고 계신 이 글도 그 플랫폼 위에서 발행되고 있습니다.)

이 시리즈는 시작과 회고의 이야기예요. 코어를 지탱하는 경계선들은 "플랫폼 코어 설계" 시리즈에서, 그 위에서 블로그라는 제품을 완성해 가는 이야기는 "쓰고, 발행하기" 시리즈부터 이어집니다.

시리즈 이어읽기

가족 블로그가 플랫폼이 된 사연

2 / 4

이야기 나누기

반응을 남기고 새 글 소식을 받아보세요.

댓글0
서로를 존중하는 대화를 남겨주세요.

첫 댓글을 남겨 대화를 시작해보세요.

새 글 구독
새 글이 공개되면 이메일로 알려드려요.