← 목록
기타 2026-06-14 24KB 읽기 29분

웹사이트 빌더/퍼블리셔 아키텍처 심화 — 크라우니 + 빠델라 랜딩

작성일: 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개
2단계: 퍼블리시
  • "퍼블리시" 클릭
  • 도메인: padella.crowny.org 선택
  • 자동 배포 완료 → https://padella.crowny.org 라이브
3단계: 프리미엄 (선택)
  • 커스텀 도메인: 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주)

  • 빠델라 랜딩 완성
  • 라이브 검증
  • 성능 최적화
총 6주 추정


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 + 도메인 자동 결선
빠델라 사례: 30분 내 비개발자가 멋진 식당 랜딩 완성, 즉시 라이브 → 예약 CTA → 매출 연결


출처

  1. Server-Driven UI: A 2026 Guide to Architecture & Examples
  2. Code Export Platform: 2026 Buyer's Guide to Avoid Lock-In
  3. Vercel Releases JSON-Render: a Generative UI Framework for AI-Driven Interface Composition
  4. Build SaaS marketing sites in Webflow using Figma design systems
  5. Design Tokens in Webstudio: A Practical Implementation Guide
  6. Responsive Breakpoint System & Design Tokens
  7. Pragmatic Drag and Drop - Atlassian
  8. React DnD - iFrame Examples