웹사이트 빌더/퍼블리셔 아키텍처 심화 — 크라우니 + 빠델라 랜딩
작성일: 2026-06-14 대상: 웹 퍼블리싱 도구 개발 (Crowny design.crowny.org 확장) + 빠델라 식당 랜딩 폴리시 핵심: MVP 드래그앤드롭 에디터, 정적 HTML 익스포트, 바닐라 JS 구현
I. 아키텍처 핵심 3계층
1. 데이터 계층: 컴포넌트 트리 JSON 스키마
모든 페이지는 불변 JSON 컴포넌트 트리로 표현된다.
json{
"id": "page_main_v1",
"type": "Page",
"breakpoints": ["mobile", "tablet", "desktop"],
"metadata": {
"title": "Padella Restaurant",
"description": "Korean-Italian Fusion",
"slug": "/index.html"
},
"sections": [
{
"id": "hero_1",
"type": "Hero",
"props": {
"backgroundImage": "/assets/hero.jpg",
"heading": "Padella",
"subheading": "우리의 이야기",
"cta": {
"text": "예약하기",
"href": "#booking"
}
},
"styles": {
"mobile": { "height": "500px", "fontSize": "24px" },
"tablet": { "height": "600px", "fontSize": "32px" },
"desktop": { "height": "800px", "fontSize": "48px" }
}
},
{
"id": "gallery_1",
"type": "Gallery",
"props": {
"columns": { "mobile": 1, "tablet": 2, "desktop": 3 },
"items": [
{ "image": "/assets/dish1.jpg", "caption": "파스타" },
{ "image": "/assets/dish2.jpg", "caption": "한우볼" }
]
}
},
{
"id": "contact_1",
"type": "ContactForm",
"props": {
"fields": ["name", "email", "message"],
"submitText": "문의",
"endpoint": "/api/contact"
}
}
]
}
핵심 원칙:
- 컴포넌트 =
{ id, type, props, styles } - Props = 콘텐츠 데이터 (텍스트, 이미지, CTA)
- Styles = 반응형 스타일 (breakpoint별)
- 중첩 구조 무제한 지원
II. 에디터 계층: 드래그앤드롭 실시간 프리뷰
2.1 핵심 기술: 이중 프레임 아키텍처
┌─────────────────────────────────────┐
│ Main Editor (바닐라 JS) │
│ ┌───────────────────────────────┐ │
│ │ Sidebar (컴포넌트 라이브러리) │ │
│ │ - Hero │ │
│ │ - Gallery │ │
│ │ - Contact Form │ │
│ └───────────────────────────────┘ │
│ ┌───────────────────────────────┐ │
│ │ Canvas (드래그 감지) │ │
│ │ (→ JSON 트리 업데이트) │ │
│ └───────────────────────────────┘ │
│ ┌───────────────────────────────┐ │
│ │ Properties Panel │ │
│ │ (선택된 컴포넌트 편집) │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
↓ PostMessage
┌─────────────────────────────────────┐
│ Preview iFrame (샌드박스) │
│ ┌───────────────────────────────┐ │
│ │ 실시간 렌더링 (컴포넌트 라이브러리) │
│ │ - Hero.js │ │
│ │ - Gallery.js │ │
│ │ - ContactForm.js │ │
│ │ (JSON 트리 → DOM) │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
2.2 iFrame 드래그앤드롭 해결책
문제: iframe 위에서 드래그하면 "ghost image"가 사라지고 이벤트가 끊김
솔루션: 오버레이 마스크
javascript// 에디터 JS (메인 윈도우)
const canvas = document.getElementById('editor-canvas');
let dragOverlay = null;
canvas.addEventListener('dragstart', (e) => {
// iframe 위의 드래그를 감지하기 위해 투명한 오버레이 생성
dragOverlay = document.createElement('div');
dragOverlay.style.cssText = `
position: fixed; top: 0; left: 0;
width: 100%; height: 100%;
z-index: 99999;
cursor: grabbing;
`;
document.body.appendChild(dragOverlay);
// iframe으로 신호 전송
previewFrame.contentWindow.postMessage({
type: 'DRAG_START',
component: e.dataTransfer.effectAllowed
}, '*');
});
dragOverlay?.addEventListener('dragover', (e) => {
e.preventDefault();
previewFrame.contentWindow.postMessage({
type: 'DRAG_OVER',
x: e.clientX,
y: e.clientY
}, '*');
});
dragOverlay?.addEventListener('drop', (e) => {
e.preventDefault();
dragOverlay.remove();
// 드롭 위치의 컴포넌트 계산 후 JSON 트리 업데이트
updateComponentTree({
action: 'INSERT',
type: e.dataTransfer.getData('componentType'),
position: calculateDropPosition(e)
});
});
2.3 실시간 프리뷰 업데이트
javascript// 에디터 JS: 트리 변경 감지
function updateComponentTree(delta) {
tree = applyDelta(tree, delta);
// iframe 프리뷰에 JSON 전송
previewFrame.contentWindow.postMessage({
type: 'TREE_UPDATE',
tree: tree,
breakpoint: currentBreakpoint // 'mobile' | 'tablet' | 'desktop'
}, '*');
// 로컬 스토리지에 저장 (임시)
localStorage.setItem('draft_tree', JSON.stringify(tree));
}
// Preview iFrame JS: 렌더링
window.addEventListener('message', (e) => {
if (e.data.type === 'TREE_UPDATE') {
const tree = e.data.tree;
const breakpoint = e.data.breakpoint;
// 루트 DOM 비우고 재렌더링
const root = document.getElementById('root');
root.innerHTML = '';
tree.sections.forEach(section => {
const component = renderComponent(section, breakpoint);
root.appendChild(component);
});
}
});
III. 컴포넌트 라이브러리 (최소 6종)
구현 전략: 바닐라 JS + 데이터 기반 렌더링
javascript// src/components/Hero.js
export function renderHero(props, styles, breakpoint) {
const container = document.createElement('section');
container.className = 'hero';
const style = styles[breakpoint] || styles.desktop;
Object.assign(container.style, {
backgroundImage: `url(${props.backgroundImage})`,
height: style.height,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center'
});
const heading = document.createElement('h1');
heading.textContent = props.heading;
heading.style.fontSize = style.fontSize;
heading.style.color = style.color || '#ffffff';
const subheading = document.createElement('p');
subheading.textContent = props.subheading;
const cta = document.createElement('a');
cta.href = props.cta.href;
cta.textContent = props.cta.text;
cta.className = 'btn btn-primary';
container.appendChild(heading);
container.appendChild(subheading);
container.appendChild(cta);
return container;
}
// src/components/Gallery.js
export function renderGallery(props, styles, breakpoint) {
const container = document.createElement('section');
container.className = 'gallery';
const style = styles[breakpoint] || styles.desktop;
container.style.display = 'grid';
container.style.gridTemplateColumns = `repeat(${props.columns[breakpoint]}, 1fr)`;
container.style.gap = style.gap || '20px';
props.items.forEach(item => {
const figure = document.createElement('figure');
const img = document.createElement('img');
img.src = item.image;
img.alt = item.caption;
img.style.width = '100%';
img.style.height = 'auto';
const caption = document.createElement('figcaption');
caption.textContent = item.caption;
figure.appendChild(img);
figure.appendChild(caption);
container.appendChild(figure);
});
return container;
}
// src/components/ContactForm.js
export function renderContactForm(props, styles, breakpoint) {
const form = document.createElement('form');
form.className = 'contact-form';
props.fields.forEach(field => {
const wrapper = document.createElement('div');
wrapper.className = 'form-group';
const label = document.createElement('label');
label.htmlFor = `field_${field}`;
label.textContent = field.charAt(0).toUpperCase() + field.slice(1);
let input;
if (field === 'message') {
input = document.createElement('textarea');
input.rows = 5;
} else {
input = document.createElement('input');
input.type = field === 'email' ? 'email' : 'text';
}
input.id = `field_${field}`;
input.name = field;
input.required = true;
wrapper.appendChild(label);
wrapper.appendChild(input);
form.appendChild(wrapper);
});
const button = document.createElement('button');
button.type = 'submit';
button.textContent = props.submitText;
button.className = 'btn btn-primary';
form.addEventListener('submit', (e) => {
e.preventDefault();
const formData = new FormData(form);
fetch(props.endpoint, {
method: 'POST',
body: JSON.stringify(Object.fromEntries(formData))
}).then(r => r.json());
});
form.appendChild(button);
return form;
}
// 다른 컴포넌트들
// - Section (제목 + 단락)
// - Testimonial (리뷰)
// - CTA (Call-to-Action 블록)
IV. 반응형 시스템 & 디자인 토큰
4.1 CSS 변수 기반 토큰
css/* src/styles/tokens.css */
:root {
/* 색상 토큰 */
--color-primary: #2c3e50;
--color-accent: #e74c3c;
--color-bg: #ffffff;
--color-text: #2c3e50;
/* 타이포그래피 토큰 */
--font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, sans-serif;
--font-size-h1: 48px;
--font-size-h2: 36px;
--font-size-body: 16px;
--line-height: 1.6;
/* 스페이싱 토큰 */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 32px;
--spacing-xl: 64px;
/* 반응형 중단점 (CSS 변수로 추적) */
--breakpoint-mobile: 320px;
--breakpoint-tablet: 768px;
--breakpoint-desktop: 1024px;
}
/* 모바일 우선 */
body {
font-size: 14px;
line-height: var(--line-height);
color: var(--color-text);
}
h1 {
font-size: 28px; /* 모바일 */
}
/* 태블릿 이상 */
@media (min-width: 768px) {
h1 {
font-size: 36px;
}
}
/* 데스크톱 이상 */
@media (min-width: 1024px) {
h1 {
font-size: 48px;
}
}
4.2 JSON 스키마의 반응형 스타일 (권장)
json{
"id": "section_1",
"type": "Section",
"props": {
"title": "메뉴",
"items": [...]
},
"styles": {
"mobile": {
"padding": "16px",
"fontSize": "14px",
"columns": 1
},
"tablet": {
"padding": "24px",
"fontSize": "16px",
"columns": 2
},
"desktop": {
"padding": "32px",
"fontSize": "18px",
"columns": 3
}
}
}
V. 정적 HTML/CSS/JS 익스포트 파이프라인
5.1 익스포트 플로우
JSON 트리 (editor)
↓
컴포넌트 렌더링 함수 실행
↓ (각 컴포넌트 → HTML 문자열)
단일 HTML 문자열
↓
인라인 CSS + JS + 메타데이터 삽입
↓
최소화 및 최적화
↓
ZIP 파일 생성 (HTML + /assets/)
↓
다운로드 또는 FTP 업로드
5.2 바닐라 JS 익스포트 구현
javascript// src/export.js
export function exportToHTML(tree) {
// 1. 마스터 HTML 템플릿
let html = `<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${tree.metadata.title}</title>
<meta name="description" content="${tree.metadata.description}">
<style>
${generateInlineCSS(tree)}
</style>
</head>
<body>
<div id="root">
${generateHTML(tree)}
</div>
<script>
${generateInlineJS(tree)}
</script>
</body>
</html>`;
return html;
}
function generateHTML(tree) {
return tree.sections.map(section => {
const component = require(`./components/${section.type}`);
// 데스크톱 기본값으로 정적 HTML 생성
return component.renderToString(section.props, section.styles, 'desktop');
}).join('\n');
}
function generateInlineCSS(tree) {
// 토큰 CSS + 글로벌 스타일 + 컴포넌트별 인라인 스타일
return `
:root {
--color-primary: #2c3e50;
--spacing-md: 16px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: Pretendard, system-ui; line-height: 1.6; }
h1 { font-size: 48px; margin: var(--spacing-md) 0; }
@media (max-width: 768px) {
h1 { font-size: 28px; }
}
`;
}
function generateInlineJS(tree) {
// 폼 처리, 모달, 메뉴 토글 등 최소 기능 JS
return `
document.querySelectorAll('form').forEach(form => {
form.addEventListener('submit', function(e) {
e.preventDefault();
const data = new FormData(this);
fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(Object.fromEntries(data))
});
});
});
`;
}
// ZIP 파일 생성 (require jszip)
export async function downloadAsZip(tree) {
const JSZip = window.JSZip || await import('jszip');
const zip = new JSZip();
// index.html 추가
zip.file('index.html', exportToHTML(tree));
// 에셋 파일들 추가 (비동기)
const assets = extractAssets(tree);
for (const [path, blob] of assets) {
zip.file(`assets/${path}`, blob);
}
// ZIP 생성 및 다운로드
const content = await zip.generateAsync({ type: 'blob' });
const url = URL.createObjectURL(content);
const link = document.createElement('a');
link.href = url;
link.download = `${tree.metadata.slug.replace('/', '')}.zip`;
link.click();
}
function extractAssets(tree) {
const assets = new Map();
function walk(section) {
if (section.props?.backgroundImage) {
const filename = section.props.backgroundImage.split('/').pop();
assets.set(filename, section.props.backgroundImage);
}
if (section.props?.items) {
section.props.items.forEach(item => {
if (item.image) {
const filename = item.image.split('/').pop();
assets.set(filename, item.image);
}
});
}
}
tree.sections.forEach(walk);
return assets;
}
VI. 퍼블리시 파이프라인 (도메인 결선)
6.1 3단계 배포
┌─────────────────────────────────────┐
│ 1. 에디터에서 "퍼블리시" 클릭 │
│ (JSON 트리 → 정적 HTML) │
└──────────────┬──────────────────────┘
↓
┌─────────────────────────────────────┐
│ 2. 도메인 선택 (빠델라 예) │
│ - padella.com (구매 필요) │
│ - padella.crowny.org (무료) │
└──────────────┬──────────────────────┘
↓
┌─────────────────────────────────────┐
│ 3. 게이트웨이 자동 결선 │
│ - DNS: padella.crowny.org │
│ - 포트: 8080 HTTP / 8443 HTTPS │
│ - CDN: /assets 캐싱 │
│ - 인증서: Let's Encrypt 자동 │
└─────────────────────────────────────┘
6.2 배포 API 엔드포인트
javascript// src/api/publish.js
export async function publishPage(tree, domain) {
const html = exportToHTML(tree);
const assets = extractAssets(tree);
// 1. 정적 파일 저장소에 업로드
const storageUrl = await uploadToStorage(html, assets, domain);
// 2. 게이트웨이에 도메인 등록
const gatewayResponse = await fetch('https://gateway.crowny.org/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
domain: domain,
target: storageUrl,
ssl: true,
caching: {
assets: 3600,
html: 300
}
})
});
return await gatewayResponse.json();
}
// 스토리지: S3, Google Cloud Storage, 또는 로컬 /var/www/pages/
async function uploadToStorage(html, assets, domain) {
const formData = new FormData();
formData.append('html', new Blob([html], { type: 'text/html' }));
formData.append('domain', domain);
assets.forEach((blob, path) => {
formData.append(`assets/${path}`, blob);
});
const response = await fetch('/api/storage/upload', {
method: 'POST',
body: formData
});
const { url } = await response.json();
return url;
}
VII. MVP 최소 기능 체크리스트
에디터 (바닐라 JS)
- 컴포넌트 팔레트 (드래그 가능)
- Canvas 영역 (드롭 감지)
- Properties 패널 (선택된 컴포넌트 편집)
- 실시간 iFrame 프리뷰 (PostMessage)
- Undo/Redo (JSON 트리 히스토리)
- 반응형 미리보기 (mobile/tablet/desktop 토글)
- 로컬 저장 (localStorage)
컴포넌트 라이브러리
- Hero (배경 이미지 + CTA)
- Section (제목 + 텍스트)
- Gallery (반응형 그리드)
- ContactForm (폼 필드 + 제출)
- Testimonial (리뷰 카드)
- CTA (액션 버튼)
익스포트
- 정적 HTML 생성
- 인라인 CSS (토큰 + 스타일)
- 인라인 JS (폼, 이벤트)
- 에셋 파일 포함
- ZIP 다운로드
퍼블리시
- 도메인 입력
- 게이트웨이 자동 결선
- SSL 인증서 자동 발급
- 라이브 URL 생성
VIII. 빠델라 식당 랜딩 폴리시
사용 시나리오
1단계: 빠델라 브랜드 매니저 (비개발자)
- Crowny 에디터 접속 → padella.crowny.org 프로젝트 생성
- Hero 섹션: 배경(우동사진) + "Padella — 우리의 이야기" + "예약" CTA
- Gallery: 파스타/한우볼/음료 사진 + 설명
- ContactForm: 예약 요청 (이름/전화/인원)
- Section: 영업시간, 주소, 전화번호
- Testimonial: 고객 리뷰 3-4개
- "퍼블리시" 클릭
- 도메인: padella.crowny.org 선택
- 자동 배포 완료 → https://padella.crowny.org 라이브
- 커스텀 도메인: padella.com 구매
- 게이트웨이에 padella.com 등록
- padella.com → padella.crowny.org 리다이렉트 자동 처리
- Google Analytics 코드 삽입 (HTML 커스텀 섹션)
예상 결과
- 제작 시간: 30분 (비개발자 기준)
- 배포 시간: 2분 (자동화)
- 유지보수: 에디터에서 실시간 수정 → "퍼블리시" → 즉시 반영
- 비용: 무료 (crowny.org) 또는 도메인비만
IX. 기술 스택 (권장)
| 계층 | 기술 | 이유 |
|---|---|---|
| 에디터 UI | 바닐라 JS + HTML5 | 의존성 최소화, 빠른 성능 |
| 컴포넌트 | JS 함수 (DOM 생성) | 번들 크기 작음, SSR 가능 |
| 스타일 | CSS 변수 + inline | 동적 테마, 외부 CDN 불필요 |
| 프리뷰 | iframe + PostMessage | 샌드박스, 안전성 |
| 저장소 | localStorage (임시) + IndexedDB (영구) | 브라우저 네이티브 |
| 익스포트 | jszip + blob 다운로드 | 클라이언트 사이드 처리 |
| 배포 | 게이트웨이 API + S3/local 스토리지 | 확장 가능, 자동화 |
X. 구현 타이ム라인 (Crowny design.crowny.org 확장)
Phase 1: 기초 에디터 (1주)
- 컴포넌트 팔레트 + Canvas
- PostMessage 프리뷰 연결
- Properties 패널
Phase 2: 컴포넌트 라이브러리 (1주)
- Hero, Section, Gallery, ContactForm, Testimonial, CTA
- 각 컴포넌트 렌더링 함수 작성
Phase 3: 익스포트 + 퍼블리시 (1주)
- HTML 생성 함수
- 게이트웨이 API 통합
- ZIP 다운로드
Phase 4: 반응형 + 고급 기능 (2주)
- 모바일/태블릿/데스크톱 토글
- Undo/Redo
- 도메인 검색 + 자동 결선
Phase 5: 빠델라 테스트 + 배포 (1주)
- 빠델라 랜딩 완성
- 라이브 검증
- 성능 최적화
XI. 참고 자료 & 유사 도구
| 도구 | 특징 | 학습 포인트 |
|---|---|---|
| Webstudio | 정적 HTML 익스포트, 오픈소스 | CSS 변수 활용, 컴포넌트 시스템 |
| TeleportHQ | 디자인→코드 변환, 깔끔한 내보내기 | JSON 중간표현, 프레임워크 중립성 |
| Webflow | 드래그앤드롭 + 호스팅 | 도메인 자동결선, CMS 통합 |
| Figma publish | 디자인 시스템 동기화 | 토큰 플로우, 컴포넌트 매핑 |
| json-render (Vercel) | LLM → JSON → UI | 컴포넌트 카탈로그, 타입 안전성 |
XII. 주의사항 및 함정
데이터 모델
❌ 피하기: 렌더링 함수 코드를 JSON에 저장 (보안 위험) ✅ 사용: Props와 스타일만 JSON에 저장, 렌더링은 JS 함수로드래그앤드롭
❌ 피하기: iframe 내부 드래그 감지만으로 충분하다고 가정 ✅ 사용: 메인 윈도우의 오버레이 마스크 + PostMessage로 처리익스포트
❌ 피하기: 모든 동적 기능을 정적 HTML에 포함시키기 (번들 과대) ✅ 사용: 필수 폼과 이벤트만 인라인, 나머지는 외부 API로반응형
❌ 피하기: 각 breakpoint마다 별도의 JSON 생성 ✅ 사용: 단일 JSON에styles: { mobile, tablet, desktop } 중첩도메인 결선
❌ 피하기: 수동 DNS, 인증서, 호스팅 설정 ✅ 사용: 게이트웨이 API 자동화 (crowny-ports.sh, ACME)결론
웹 퍼블리셔는 JSON 컴포넌트 트리 + 바닐라 JS 렌더러 + 게이트웨이 자동화의 삼각형으로 작동한다.
- 에디터: 드래그앤드롭으로 JSON 수정
- 프리뷰: iframe PostMessage로 실시간 반영
- 익스포트: 정적 HTML + 토큰 CSS + 최소 JS
- 퍼블리시: 게이트웨이 API + 도메인 자동 결선
출처
- Server-Driven UI: A 2026 Guide to Architecture & Examples
- Code Export Platform: 2026 Buyer's Guide to Avoid Lock-In
- Vercel Releases JSON-Render: a Generative UI Framework for AI-Driven Interface Composition
- Build SaaS marketing sites in Webflow using Figma design systems
- Design Tokens in Webstudio: A Practical Implementation Guide
- Responsive Breakpoint System & Design Tokens
- Pragmatic Drag and Drop - Atlassian
- React DnD - iFrame Examples