TRENDSHIFT 딥다이브 · 2026-07-01

Instatic 딥다이브
— 편집기·엔진·퍼블리셔가 서버 하나에 다 들어있는 셀프호스팅 CMS

Instatic은 비주얼 편집기 + 콘텐츠 엔진 + 퍼블리셔가 전부 Bun 서버 하나에 들어있는 셀프호스팅 CMS (Content Management System, 콘텐츠 관리 시스템)다. 보통 사이트 하나를 만들려면 헤드리스 CMS·프레임워크·호스팅·폼 서비스·이미지 CDN을 따로따로 붙여야 하는데, Instatic은 그 반대 베팅을 한다 — 캔버스 편집기부터 발행까지 한 프로세스가 다 처리하고, 발행된 페이지는 프레임워크 런타임이 섞이지 않은 순수 HTML/CSS로 나온다. Railway 원클릭 배포로 약 2분이면 뜨고, SQLite 또는 Postgres 중 고를 수 있다. 아직 0.0.x 버전의 이른 단계 프로젝트다. (저장소: CoreBunch/Instatic · TypeScript · ★1,558 · MIT)
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

이 레포가 무엇을 하는 물건인가.

핵심 메시지

"건축사무소 따로, 시공사 따로, 관리사무소 따로 부르지 말고,
설계·시공·입주 관리를 다 하는 종합건설사 하나만 부르자."

보통 웹사이트 하나를 만들려면 CMS·프레임워크·호스팅·폼 서비스·이미지 CDN을 따로 계약하고, 각자 청구서와 대시보드와 새벽 장애를 하나씩 떠안는다. Instatic은 그 전부를 Bun 서버 한 대 안에 욱여넣고, 결과물은 view-source로 열어도 부끄럽지 않은 깨끗한 HTML로 낸다.

Instatic은 비주얼 캔버스 편집기, 콘텐츠 엔진, 퍼블리셔(발행기)가 전부 하나의 Bun 서버 프로세스로 돌아가는 셀프호스팅 CMS다. 어디서든 원하는 곳에 직접 띄울 수 있고, 데이터베이스는 SQLite 또는 Postgres 중 고른다. README의 표현을 빌리면 "일반적인 현대 웹사이트는 스택을 조립하는 일" — 헤드리스 CMS 하나, 프레임워크 하나, 호스트 하나, 폼 서비스 하나, 애널리틱스 벤더 하나, 이미지 CDN 하나, 각각 자기 청구서와 대시보드와 새벽 2시 장애를 달고 온다. Instatic은 그 반대편에 선다.

더 중요한 건 결과물이다. 발행된 페이지는 군더더기 없는 시맨틱(의미론적) HTML과 압축된 CSS로만 나가고, 편집기가 남긴 흔적(React 런타임, 빌더 속성, div 수프)이 전혀 없다. 대부분의 경우 페이지는 정적 파일 그 자체다. 명령어 하나 없이 git clone → bun install → bun run dev면 로컬에서 뜨고, 실제 서비스는 Railway 템플릿으로 원클릭 배포하면 약 2분 안에 살아난다.

용어
셀프호스팅 (Self-hosted)
서비스 업체의 서버가 아니라, 내 서버(VPS·홈서버·클라우드 인스턴스 등)에 직접 설치해서 돌리는 방식이다. 월 구독료 대신 내 인프라 비용만 들고, 데이터도 전부 내 손 안에 있다. 대신 업데이트·백업·보안은 내가 챙겨야 한다. Instatic은 "MIT 라이선스 · 셀프호스팅 · 당신 것"을 슬로건처럼 내세운다 — 요금제 등급도, 오픈코어 자물쇠도, "영업팀에 문의하세요"도 없다.

2왜 주목받는가

트렌딩 이유 · 경쟁 제품 대비 장점.

Instatic은 2026년 4월 말에 처음 공개된, 아직 2개월 남짓 된 신생 프로젝트인데도 ★1,558개 · 포크 138개를 모았다. 빠르게 주목받는 이유는 크게 세 가지다.

① "이거 하나면 끝" 이라는 약속을 실제로 지킨다. 디자인(캔버스 + 디자인 토큰 엔진 Core Framework 내장) · 빌드(모듈·컴포넌트·템플릿·루프) · 관리(통합 데이터 모델, 롤 기반 접근 제어) · 분석(대시보드·감사 로그)까지 네 축을 한 제품이 다 맡는다. 웹빌더(Webflow류)는 콘텐츠 모델링이 약하고, 헤드리스 CMS(Strapi류)는 프론트엔드를 따로 짜야 한다.

② 발행 결과물이 "프레임워크 프리(free)"다. 캔버스에서 아무리 화려하게 편집해도, 방문자에게 나가는 페이지에는 React도, 편집기 런타임도, 하이드레이션(수화) 단계도 없다. 정적으로 구운 HTML 파일이거나, 극히 일부만 초경량(~668바이트) 런타임으로 지연 로드된다.

③ 플러그인이 진짜로 격리돼 있다. 대부분의 CMS 플러그인 생태계는 "플러그인 하나가 사이트 전체를 무너뜨린다"는 공포 스토리를 갖고 있다. Instatic 플러그인은 QuickJS-WASM 샌드박스 안에서만 돈다 — 파일시스템도, 환경변수도, 네트워크도 기본은 전부 차단이고 사이트 소유자가 하나씩 승인해야 열린다.

비교 대상일반적인 접근Instatic의 접근
스택 구성CMS + 프레임워크 + 호스팅 + 폼 서비스 + CDN을 각각 계약Bun 서버 하나가 편집기·엔진·퍼블리셔·폼·미디어를 전부 처리
발행 결과물프레임워크 런타임·하이드레이션이 페이지에 섞여 나감순수 시맨틱 HTML + 압축 CSS, 정적 파일에 가까움
콘텐츠 모델페이지·포스트·커스텀 필드가 각기 다른 테이블/개념페이지·포스트·컴포넌트가 전부 data_tables+data_rows 하나
플러그인 안전성호스트 프로세스 안에서 그대로 실행(전권 위험)QuickJS-WASM 샌드박스, 권한은 명시적 승인제
기존 방식의 한계
"조립형 스택"의 숨은 비용

헤드리스 CMS + 별도 프론트엔드 프레임워크 조합은 유연하지만, 서비스가 늘어날수록 청구서·대시보드·장애 지점도 함께 늘어난다. 콘텐츠 모델과 디자인 시스템이 서로 다른 도구에 흩어져 있으면 "색상 토큰 하나 바꾸는 일"조차 여러 시스템을 오가야 한다.

Instatic의 해결
"한 프로세스, 한 콘텐츠 모델, 한 청구서"

편집기·엔진·발행이 같은 Bun 프로세스에서 돌고, 페이지·포스트·컴포넌트가 같은 데이터 모델을 공유한다. 디자인 토큰(Core Framework)도 데이터로 저장돼, 토큰 하나를 바꾸면 그 토큰을 쓰는 모든 페이지가 즉시 갱신된다. 결과는 host 하나, 결제 하나, 그리고 view-source로 열어도 되는 페이지.

3기술 스택 전체 지도

백엔드·프론트엔드·인프라 한눈에.

전체 코드베이스는 TypeScript 5,000여 개 소스 파일로 이루어져 있고, zod 같은 흔한 검증 라이브러리 대신 TypeBox 하나로 모든 "타입 없는 경계"(HTTP 응답, DB에 저장되는 JSON, 플러그인 매니페스트, 설정)를 통일했다. 이 규칙은 테스트(ai-driver-isolation.test.ts)로 강제된다 — 문서가 아니라 코드로 지키는 컨벤션이다.

레이어기술 / 버전역할
런타임Bun ≥1.3.0 <1.4.0서버 실행 + 패키지 관리 + 빌드 도구 전부.
언어TypeScript (전 영역)서버·어드민·에디터·플러그인 SDK 전부 타입 안전.
서버Bun.serve + 자체 라우터Express 같은 프레임워크 없이 손으로 짠 라우팅(server/router.ts).
어드민 UIReact 19 (React Compiler on) + Vite 8편집기·대시보드 SPA. 발행 페이지에는 섞이지 않음.
상태 관리Zustand + Mutative에디터 스토어. 불변성 갱신을 Mutative(구조적 공유)로 처리.
DBSQLite(bun:sqlite) 또는 Postgres(Bun.sql)DATABASE_URL로 선택. 리포지토리는 두 엔진 모두에 방언 중립적.
검증TypeBox (zod 전면 금지)모든 미검증 경계에서 스키마가 진실의 원천.
리치 텍스트Tiptap 3 (ProseMirror 기반)테이블·정렬 등 확장을 붙인 에디터.
코드 에디터CodeMirror 6CSS/HTML/JS/JSON/Markdown 구문 강조.
드래그앤드롭@dnd-kit캔버스에서 모듈을 끌어다 배치.
이미지 처리sharp + BlurHash리사이즈·WebP 인코딩. 별도 Bun.Worker에서 처리.
플러그인 샌드박스quickjs-emscripten (QuickJS-WASM)플러그인 서버 코드를 완전 격리 실행.
AI 연동@modelcontextprotocol/sdk + 자체 HTTP/SSE 드라이버Claude·OpenAI·OpenRouter·로컬 Ollama를 벤더 SDK 없이 REST로 직접 호출.
인프라Docker + Caddy단일 이미지 배포. Caddy가 자동 HTTPS·리버스 프록시 담당.
배포Railway 원클릭 템플릿SQLite/Postgres 템플릿 각각 제공, 볼륨·헬스체크 자동 구성.
테스트bun test + Playwright단위/통합 테스트 + E2E. 아키텍처 규칙도 테스트로 검사.
용어
TypeBox
TypeScript 타입을 런타임에도 검사할 수 있는 JSON Schema로 동시에 정의하는 라이브러리다. "이 데이터는 이런 모양이어야 해"를 한 번만 적으면, 컴파일 타임 타입 체크와 실행 중 검증(예: API 요청 바디, 플러그인 매니페스트)이 같은 정의를 공유한다. Instatic은 이걸 전면 채택하고 zod를 코드베이스에서 아예 제거했다 — "검증 라이브러리가 두 개 섞이면 결국 하나는 거짓말을 한다"는 판단.

4아키텍처 심화 분석

먼저 전체 그림, 그다음 "발행" 흐름 한 줄기를 끝까지 따라가기.

Instatic은 단일 Bun 프로세스가 방문자·어드민·API·스트리밍 AI 에이전트·업로드까지 전부 서빙한다. 메인 스레드를 막을 수 있는 무거운 작업 두 가지만 Bun.Worker(별도 스레드)로 빼냈다 — 이미지 변환(sharp+BlurHash)과 플러그인 서버 코드(QuickJS 샌드박스)다. 먼저 숲을 보자.

┌────────────────────────── Bun 프로세스 (server/index.ts) ──────────────────────────┐ │ │ │ 요청 ──▶ server/router.ts ──▶ ┌───────────────┬──────────────┬────────────────┐ │ │ │ CMS 핸들러 │ 정적 SPA │ 발행된 페이지 │ │ │ │ /admin/api/* │ /admin/* │ / (그 외 전부) │ │ │ │ ├▶ auth │ → dist/ │ → publicRouter │ │ │ │ ├▶ repositories │ → uploads/ │ │ │ │ └▶ db/client (PG or SQLite) │ │ │ │ └───────────────┴──────────────┴────────────────┘ │ │ │ │ ┌─ Bun.Worker 풀 ───────────────────┐ ┌─ Bun.Worker (플러그인당 1개) ──────────┐ │ │ │ 이미지 변환: sharp + BlurHash │ │ QuickJS-WASM 샌드박스 │ │ │ │ (메인 스레드 CPU 부담 off-load) │ │ (호스트는 플러그인 코드를 직접 import │ │ │ │ │ │ 하지 않음 — 크래시해도 그 플러그인만) │ │ │ └───────────────────────────────────┘ └────────────────────────────────────┘ │ └────────────────────────────────────────────────────────────────────────────────────┘

이제 대표 흐름 한 줄기를 손으로 따라가 보자. 방문자가 발행된 페이지 URL을 열었을 때 — 즉 "그 외 전부"로 빠지는 가장 흔한 경로다. 핵심 5%(해피 패스)만 본다.

① 방문자가 GET /some-page 요청 ② server/publish/publicRouter.ts : renderPublicResolution() 이 URL을 해석 ③ Layer A 먼저 확인: uploads/published/current/some-page.html 이 이미 구워져 있나? └▶ 있으면 그 파일을 그대로 반환 (DB 왕복 없음, 가장 빠른 경로) ④ 없으면(동적 라우트) Layer B 인메모리 LRU 캐시(urlPath+쿼리 키) 확인 └▶ 캐시 적중이면 그대로 반환 ⑤ 캐시도 없으면 실제 렌더: publishPage(page, site, registry) 호출 ⑥ renderNode()가 페이지 트리를 하위→상위로 순회하며 각 모듈의 render() 호출 ⑦ 렌더 중 "요청마다 달라지는" 노드는 Layer C <instatic-hole> 자리표시자로 남김 ⑧ 완성된 HTML을 플러그인 필터 통과시켜 응답 → 브라우저가 그린다 ⑨ 화면에 뜬 <instatic-hole>은 ~668바이트 러타임이 IntersectionObserver로 감지해 /_instatic/hole/<nodeId> 를 따로 불러와 채운다

이 프로젝트 특유의 "정상 모양새"를 짚자면: 저작 시점엔 아무도 "이건 동적이야"라고 토글하지 않는다. src/core/publisher/dynamicDetection.tsfindDynamicNodeIds 하나가 노드 트리를 한 번 훑으며 4가지 규칙(동적 플래그·요청 의존 바인딩·루프 소스·동적 콘텐츠를 포함한 VC 참조)으로 자동 분류한다. 이 결과가 Layer A(빈 집합이면 완전 정적 굽기)와 Layer C(자리표시자 발행) 둘 다에 쓰인다 — 판정 로직이 하나뿐이라 "정적인 줄 알았는데 사실 동적이었다" 류의 불일치가 구조적으로 생기지 않는다.

설계 패턴
3계층 발행 파이프라인 (Layer A/B/C)
Layer A는 완전 정적 페이지를 발행 시점에 디스크로 미리 구워(2-슬롯 심볼릭 링크 원자적 교체) 방문자에겐 파일 하나만 서빙한다. Layer B는 동적 라우트용 인메모리 LRU — 항목마다 버전을 붙여, 렌더 시작 시점의 버전을 캐치해 두면 그사이 새 발행이 끼어들어도 오래된 결과를 캐싱하지 않는다. Layer C는 진짜 요청별로 달라지는 조각만 <instatic-hole>로 지연 로드한다. 세 계층 모두 "저자가 손으로 지정"하지 않고 자동 분류에 의존하는 게 핵심 설계 결정.
설계 패턴
통합 콘텐츠 모델 — data_tables / data_rows
페이지·포스트·비주얼 컴포넌트·커스텀 컬렉션이 전부 같은 두 테이블에 산다. 별도로 구석에 숨은 "pages" 테이블이 없다. 그래서 스키마·원본 행·임포트·익스포트·폼 제출까지 한 곳에 일관되게 놓인다. 페이지 트리(Page)와 비주얼 컴포넌트 트리(VC)도 같은 NodeTree<TNode> 자료구조 하나를 공유한다 — "트리를 다루는 코드"를 한 번만 짜면 두 군데서 다 쓴다는 뜻.

5디렉토리 구조 해부

어떤 폴더가 무슨 일을 하나. "기능별"이 아니라 "책임별"로 나뉜 게 특징.

README는 이렇게 못 박는다: "이 코드베이스는 오피니언(주관)이 강하고, 그 오피니언은 코드로 강제된다." 실제로 src/__tests__/architecture/에는 폴더 간 의존 방향을 검사하는 테스트가 진짜로 있다 — 문서가 아니라 bun test가 구조를 지킨다.

instatic/ ├── server/ Bun 서버: 라우터·핸들러·리포지토리·플러그인 런타임·DB │ ├── router.ts 모든 URL의 최초 분기점 │ ├── db/ postgres.ts · sqlite.ts · migrations-*.ts (방언별 동일 ID) │ ├── handlers/cms/ 리소스별 REST 핸들러 (pages, media, plugins …) │ ├── repositories/ DB 접근 계층 — 방언 중립 ANSI SQL만 사용 │ ├── publish/ 퍼블리셔 서버측: publicRouter · renderCache · staticArtefact │ ├── plugins/ QuickJS 샌드박스 호스트 (server/plugins/quickjs/) │ └── ai/ AI 에이전트: drivers·tools·mcp·pricing ├── src/ │ ├── core/ 엔진: 페이지 트리 · 퍼블리셔 · 플러그인 SDK · 영속성 │ │ ├── publisher/ renderNode.ts — publishPage()의 핵심 재귀 함수 │ │ ├── page-tree/ NodeTree<TNode> 트리 원시 자료구조 │ │ ├── plugin-sdk/ 플러그인 저자용 API (api.plugin.* / api.cms.*) │ │ └── framework/ Core Framework 색상/타이포/스페이싱 토큰 엔진 │ ├── admin/ 어드민 앱 셸(인증·내비게이션·워크스페이스) │ │ └── pages/site/ 비주얼 에디터(캔버스·패널·툴바·스토어) │ ├── modules/base/ 컨테이너·텍스트·이미지·버튼·폼 등 1st-party 블록 │ ├── ui/ 공유 UI 프리미티브 (Button·Input·Tree …) │ └── __tests__/architecture/ 구조 규칙을 실제로 검사하는 게이트 테스트 ├── examples/plugins/template/ 플러그인 스캐폴드 예제 ├── docs/ 아키텍처·에디터·서버·배포 문서 (직접 근거 자료) ├── Dockerfile · compose.*.yml 프로덕션 배포 설정 └── package.json bun install 대상, 스크립트 정의
파일/폴더역할
server/index.ts진입점 — DB 마이그레이션, 역할 동기화, 플러그인 활성화 후 Bun.serve 기동.
src/core/publisher/render.ts핵심 — publishPage(). 페이지 트리를 HTML/CSS 문서로 바꾸는 진입 함수.
server/publish/publicRouter.ts모든 방문자 요청의 관문. Layer A→B→라이브 렌더 순서로 시도.
src/core/plugin-sdk/플러그인 저자가 보는 API 표면. 여기가 "계약서"다.
src/__tests__/architecture/폴더 간 의존 규칙을 실제로 검사 — 구조 부패를 코드로 막는다.

6학습 포인트 (기술별)

이 레포에서 훔쳐 배울 만한 것 + 어디를 보면 되는지.

포인트 1

"CMS 발행"을 순수 함수로 설계하기

Instatic의 모듈 render()DOM도, React도, 부수효과도 없는 순수 함수다(코드베이스 안에서 "Constraint #179"라는 번호로 관리되는 규칙). 입력(props+자식 HTML)이 같으면 출력이 항상 같다 — 그래서 서버에서 안전하게, 캐시 가능하게, 테스트하기 쉽게 페이지를 굽는다. React 컴포넌트를 짜 봤다면 "왜 렌더 함수에 부수효과를 넣으면 안 되는가"를 실전 CMS 규모로 체감할 수 있다. src/core/publisher/renderNode.ts부터 읽으면 된다.

포인트 2

플러그인 격리를 QuickJS-WASM으로 실전 구현

"플러그인은 위험하다"는 걸 문서로 경고하는 대신, 물리적으로 못 하게 만든 사례다. 서버 엔트리포인트는 Bun.Worker 안의 QuickJS 가상머신에서 돌고, 파일시스템·환경변수·네트워크가 기본 전부 차단이다. 네트워크가 필요하면 networkAllowedHosts 화이트리스트를 명시적으로 열어야 한다. 브라우저 밖에서 "신뢰 안 되는 코드를 안전하게 실행하는 법"을 배우고 싶다면 server/plugins/quickjs/vm.ts가 좋은 교재다.

포인트 3 ★

"자동 분류"로 저자의 실수를 원천 차단하기

보통 CMS는 "이 블록은 캐시하지 마세요" 체크박스를 저자에게 맡긴다. Instatic은 그 체크박스 자체를 없앴다. dynamicDetection.tsfindDynamicNodeIds 하나가 트리를 훑어 정적/동적을 판정하고, 그 결과를 Layer A와 Layer C가 같이 쓴다. "사람이 깜빡할 수 있는 설정"을 아예 없애고 로직으로 대체하는 이 패턴은 이 레포 전체에 반복된다 — 배울 가치가 있는 설계 습관이다.

포인트 4

TypeBox로 "검증 라이브러리 하나만" 강제하기

런타임 검증 라이브러리가 두 개 이상 섞인 코드베이스는 결국 어느 한쪽이 타입을 거짓말하게 된다. Instatic은 zod를 코드에서 완전히 뽑아내고 TypeBox 하나로 통일한 뒤, 그 규칙을 ai-driver-isolation.test.ts 같은 실제 테스트로 게이트 걸었다. "컨벤션 문서"가 아니라 "떨어지는 CI"로 규칙을 지키는 법을 볼 수 있다.

플러그인이 CMS와 통신하는 API 표면 감을 잡으려면 SDK 시그니처를 훑어보자(개념 예시):

// src/core/plugin-sdk/ 의 API 형태(개념 요약)
api.cms.routes.register({ path: '/hello', method: 'GET' }, handler)
api.cms.storage.get(key)              // 플러그인 전용 격리 저장소
api.cms.loops.registerSource(name, fn) // 루프가 끌어올 데이터 소스 추가
api.plugin.log('info', '설치 완료')     // 호스트 로그로 전달(직접 console 접근 불가)

7시스템 요구사항

순수 소프트웨어라 특수 하드웨어는 필요 없다. 로컬 개발과 배포 기준.

항목요구사항
런타임Bun ≥1.3.0 <1.4.0. 로컬 개발엔 이것만 있으면 됨(Node.js 불필요).
데이터베이스기본 SQLite(추가 서비스 없이 .tmp/dev.db). 다중 저자·관리형 백업이 필요하면 Postgres.
배포(권장)Railway 원클릭 템플릿 — 시크릿 키 생성, 볼륨 부착, 헬스체크까지 자동. 터미널 안 열어도 됨.
자체 인프라단일 Docker 이미지(ghcr.io/corebunch/instatic) + compose.prod.yml. VPS·Fly.io·Render 문서는 docs/deployment/.
리버스 프록시/HTTPS자체 호스팅 시 Caddy 권장(자동 인증서). Caddyfile 기본 제공.
브라우저(관리자용)최신 데스크톱 브라우저 — 캔버스 편집기가 최신 CSS/JS 기능을 사용.
AI 에이전트 기능(선택)Claude·OpenAI·OpenRouter API 키 또는 로컬 Ollama. 없어도 CMS 본체는 정상 동작.

8직접 해볼 수 있는 실습 과제

읽기만 하지 말고 손에 익히는 단계. 난이도순.

실습 1

로컬에 띄우고 view-source 확인하기 난이도 ★☆☆ 입문

git clonebun installbun run devlocalhost:5173을 연다. 온보딩을 따라 사이트/소유자 계정을 만들고, 아무 페이지를 캔버스에서 편집한 뒤 발행해 본다. 그다음 발행된 페이지에서 브라우저 "페이지 소스 보기"를 열어 React나 편집기 흔적이 정말 없는지 눈으로 확인하는 게 목표.

실습 2

데이터 테이블 만들어 루프로 렌더링 난이도 ★★☆ 중급

/admin/data에서 커스텀 데이터 테이블(예: "팀원" — 이름·직함·사진 필드)을 만들고 행을 몇 개 채운다. 그다음 캔버스에서 루프(Loop) 모듈을 페이지에 놓고 이 테이블을 소스로 연결해, 카드 여러 장이 자동으로 찍혀 나오는 걸 확인한다. 변형(variant) 두 개를 주고 번갈아 나오는지도 관찰.

실습 3

Core Framework 토큰 하나 바꿔서 전 사이트 갱신 확인 난이도 ★★☆ 중급

디자인 패널에서 브랜드 색상 토큰 하나를 바꿔 본다. 그 토큰을 참조하는 여러 페이지가 재발행 없이도 즉시 반영되는지, 그리고 자동 생성된 명도 스케일(더 밝은/어두운 틴트)이 어떻게 계산되는지 관찰한다. "디자인 시스템이 데이터로 저장된다"는 게 무슨 뜻인지 체감하는 실습.

실습 4

템플릿 플러그인으로 나만의 플러그인 만들기 난이도 ★★★ 고급

examples/plugins/template/를 복사해 bun instatic-plugin dev로 실행 중인 CMS에 핫싱크한다. plugin.json에 권한(cms.routes 등)을 선언하고, 간단한 서버 라우트 하나를 추가해 본다. 권한을 선언하지 않은 API를 호출했을 때 실제로 막히는지 확인하면 샌드박스 격리를 체감할 수 있다.

실습 5

정적/동적 자동 분류 로직 직접 읽고 검증하기 난이도 ★★★ 고급

src/core/publisher/dynamicDetection.tsfindDynamicNodeIds를 정독한 뒤, 루프가 있는 페이지와 없는 페이지를 각각 발행해 uploads/published/current/에 실제로 파일이 구워지는지(Layer A 적중) 또는 <instatic-hole>이 남는지(Layer C 발동) 비교한다. 판정 결과가 예상과 다르면 어떤 규칙이 걸렸는지 코드로 역추적해 본다.

9관련 기술 심화 학습 로드맵

한 주씩, 이 레포를 교보재로 삼아 따라가는 계획.

주차주제학습 자료 / 볼 곳
1주차CMS 큰 그림 + 로컬 실행README · docs/architecture.md · bun run dev로 온보딩 체험
2주차Bun 서버 + 손으로 짠 라우터server/index.ts · server/router.ts · Bun 공식 문서(Bun.serve)
3주차퍼블리셔 — 페이지 트리 → HTMLdocs/features/publisher.md · src/core/publisher/render.ts·renderNode.ts
4주차3계층 캐시(Layer A/B/C) 원리server/publish/publicRouter.ts · dynamicDetection.ts
5주차플러그인 샌드박스 & QuickJS-WASMdocs/features/plugin-system.md · server/plugins/quickjs/vm.ts · quickjs-emscripten 문서
6주차TypeBox 검증 패턴 + 내 프로젝트에 이식TypeBox 공식 문서 · src/core/*-schema/ 폴더들 비교

10핵심 키워드 사전

본문에 나온 용어 빠른 참조.

용어의미
BunJavaScript/TypeScript 런타임 + 패키지 매니저 + 번들러. Node.js 대체재로, Instatic은 서버·빌드·테스트 전부 Bun 하나로 처리.
퍼블리셔 (Publisher)페이지 트리를 실제 HTML/CSS 문서로 바꾸는 엔진. 진입점은 publishPage().
Layer A / B / C발행 파이프라인 3계층 — 정적 굽기(디스크) / 인메모리 LRU(동적 라우트) / 지연 로드 자리표시자(요청별 조각).
<instatic-hole>요청마다 달라지는 조각을 표시하는 자리표시자 태그. ~668바이트 러타임이 IntersectionObserver로 감지해 채운다.
dynamicDetection노드가 정적인지 동적인지 자동 판정하는 단일 워커. 저자가 수동으로 토글하지 않는다.
data_tables / data_rows페이지·포스트·컴포넌트·커스텀 컬렉션이 전부 공유하는 통합 콘텐츠 저장 테이블 두 개.
NodeTree<TNode>페이지 트리와 비주얼 컴포넌트 트리가 공유하는 트리 자료구조 원시 타입.
TypeBoxTypeScript 타입 정의와 런타임 JSON Schema 검증을 동시에 제공하는 라이브러리. Instatic 전역 표준.
QuickJS-WASM경량 JS 엔진을 WebAssembly로 컴파일한 것. 플러그인 서버 코드를 호스트와 완전히 격리해 실행.
Visual Component (VC)타입이 있는 파라미터와 이름 붙은 슬롯을 가진 재사용 가능한 컴포넌트. 하나 고치면 모든 인스턴스가 갱신.
루프 (Loop)포스트·페이지·미디어 등 컬렉션을 반복해 레이아웃을 찍어내는 모듈. 변형(variant)을 번갈아 배치 가능.
Core FrameworkInstatic에 내장된 디자인 토큰 엔진 — 색상 명도 스케일·유동 타이포 스케일·스페이싱 스케일을 자동 생성.
advisory lock (권고 잠금)Postgres의 pg_try_advisory_lock 기반 리더 선출. 다중 인스턴스가 스케줄 작업을 중복 실행하지 않게 막음.
MCP (Model Context Protocol)외부 AI 클라이언트가 CMS 도구를 표준 방식으로 쓸 수 있게 하는 커넥터 프로토콜.

11참고 링크