런타임 수정
코드 변경이나 재빌드 없이 콘텐츠 유형을 생성하고 편집하세요. 비개발자도 관리자 UI를 통해 자신의 콘텐츠 모델을 설계할 수 있습니다.
EmDash는 스키마 정의가 코드가 아닌 데이터베이스에 존재하는 데이터베이스 우선 콘텐츠 모델을 사용합니다. 이는 런타임 스키마 수정과 비개발자 친화적인 설정을 가능하게 하는 근본적인 설계 선택입니다.
Strapi나 Keystatic과 같은 전통적인 CMS는 스키마를 코드로 정의하도록 요구합니다:
// Traditional approach - schema in codeconst posts = collection({ fields: { title: text({ required: true }), content: richText(), },});EmDash는 동일한 정보를 데이터베이스 테이블에 저장합니다:
-- _emdash_collections tableINSERT INTO _emdash_collections (slug, label)VALUES ('posts', 'Blog Posts');
-- _emdash_fields 테이블INSERT INTO _emdash_fields (collection_id, slug, type, required)VALUES ('coll_abc', 'title', 'string', true), ('coll_abc', 'content', 'portableText', false);두 접근 방식 모두 동일한 콘텐츠 구조를 정의합니다. 차이점은 그 구조가 어디에 존재하며 어떻게 수정될 수 있는지에 있습니다.
런타임 수정
코드 변경이나 재빌드 없이 콘텐츠 유형을 생성하고 편집하세요. 비개발자도 관리자 UI를 통해 자신의 콘텐츠 모델을 설계할 수 있습니다.
실제 SQL 컬럼
WordPress의 EAV(Entity-Attribute-Value) 모델과 달리, 각 필드는 실제 컬럼을 가집니다. 적절한 인덱싱, 외래 키, 쿼리 최적화가 가능합니다.
자체 문서화
데이터베이스 도구가 스키마를 직접 검사할 수 있습니다. 콘텐츠 모델을 이해하기 위해 코드를 파싱할 필요가 없습니다.
마이그레이션 경로
버전 관리를 위해 스키마를 JSON으로 내보내세요. 새로운 환경에서 스키마를 가져오세요.
두 개의 시스템 테이블이 콘텐츠 구조를 정의합니다:
CREATE TABLE _emdash_collections ( id TEXT PRIMARY KEY, slug TEXT UNIQUE NOT NULL, -- "posts", "products" label TEXT NOT NULL, -- "Blog Posts" label_singular TEXT, -- "Post" description TEXT, icon TEXT, -- Lucide icon name supports JSON, -- ["drafts", "revisions", "preview"] source TEXT, -- How it was created created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT);source 필드는 컬렉션이 어떻게 생성되었는지 추적합니다:
| 소스 | 설명 |
|---|---|
manual | 관리자 UI를 통해 생성됨 |
template:blog | 템플릿의 시드 파일에 의해 생성됨 |
import:wordpress | WordPress에서 가져옴 |
discovered | 기존 데이터에서 자동 발견됨 |
CREATE TABLE _emdash_fields ( id TEXT PRIMARY KEY, collection_id TEXT REFERENCES _emdash_collections(id), slug TEXT NOT NULL, -- Column name: "title", "price" label TEXT NOT NULL, -- Display label type TEXT NOT NULL, -- Field type column_type TEXT NOT NULL, -- SQLite type: TEXT, REAL, INTEGER, JSON required INTEGER DEFAULT 0, unique_field INTEGER DEFAULT 0, default_value TEXT, -- JSON-encoded default validation JSON, -- Validation rules widget TEXT, -- Custom widget identifier options JSON, -- Widget options sort_order INTEGER, created_at TEXT DEFAULT CURRENT_TIMESTAMP, UNIQUE(collection_id, slug));각 컬렉션은 ec_ 접두사를 가진 자체 테이블을 얻습니다. “products” 컬렉션을 title과 price 필드로 생성할 때:
CREATE TABLE ec_products ( -- System columns (always present) id TEXT PRIMARY KEY, slug TEXT UNIQUE, status TEXT DEFAULT 'draft', author_id TEXT, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')), published_at TEXT, deleted_at TEXT, -- Soft delete version INTEGER DEFAULT 1, -- Optimistic locking
-- 필드 정의에서 온 콘텐츠 컬럼 title TEXT NOT NULL, price REAL);관리자 UI를 통해 필드를 추가할 때, EmDash는:
_emdash_fields에 레코드를 삽입합니다 2. ALTER TABLE ec_collection ADD COLUMN column_name TYPE을 실행합니다 3. 검증을 위한 Zod 스키마를 재생성합니다SQLite는 런타임에 다음과 같은 ALTER TABLE 작업을 지원합니다:
| 작업 | 지원 여부 |
|---|---|
| 컬럼 추가 | 예 |
| 컬럼 이름 변경 | 예 |
| 컬럼 삭제 | 예 (SQLite 3.35+) |
| 컬럼 유형 변경 | 아니요 (테이블 재구성 필요) |
유형 변경의 경우, EmDash는 테이블 재구성을 투명하게 처리합니다: 새 테이블 생성 → 데이터 복사 → 기존 테이블 삭제 → 새 테이블 이름 변경.
EmDash는 명확한 분리를 유지합니다:
| 관심사 | 위치 | 테이블 |
|---|---|---|
| 스키마 | 시스템 테이블 | _emdash_collections, _emdash_fields |
| 콘텐츠 | 컬렉션별 테이블 | ec_posts, ec_products 등 |
| 미디어 | 별도 테이블 + 스토리지 | media 테이블 + R2/S3 |
| 설정 | 옵션 테이블 | site: 접두사를 가진 options |
이 분리는 다음을 의미합니다:
EmDash는 시작 시 데이터베이스 필드 정의에서 Zod 스키마를 구축합니다:
// Simplified examplefunction buildSchema(fields: Field[]): ZodSchema { const shape: Record<string, ZodType> = {};
for (const field of fields) { let zodType = fieldTypeToZod(field.type);
if (field.required) { zodType = zodType.required(); }
if (field.validation?.min !== undefined) { zodType = zodType.min(field.validation.min); }
shape[field.slug] = zodType; }
return z.object(shape);}콘텐츠는 모든 생성 및 업데이트 작업에서 이러한 런타임 스키마에 대해 검증됩니다.
데이터베이스 스키마에서 TypeScript 타입을 생성하세요:
# Fetch schema from database, generate typesnpx emdash types이것은 .emdash/types.ts를 생성합니다:
// .emdash/types.ts (generated)export interface Post { title: string; content: PortableTextBlock[]; excerpt?: string; featuredImage?: string;}
export interface Product { title: string; price: number; quantity: number;}
// 쿼리 함수에 대한 타입 오버로드declare module "emdash" { export function getEmDashCollection( type: "posts", ): Promise<{ entries: ContentEntry<Post>[]; error?: Error }>;
export function getEmDashEntry( type: "products", id: string, ): Promise<{ entry: ContentEntry<Product> | null; error?: Error; isPreview: boolean }>;}개발자는 CLI를 사용할 수 있습니다:
# Fetch schema, generate typesnpx emdash types
# 스키마를 JSON으로 내보내기npx emdash export-seed > seed.json비개발자는 관리자 UI만 독점적으로 사용합니다:
두 접근 방식 모두 동일한 기본 데이터베이스 테이블을 수정합니다.
템플릿과 내보내기는 이식 가능한 스키마 정의를 위해 JSON 시드 파일을 사용합니다:
{ "version": "1", "collections": [ { "slug": "posts", "label": "Blog Posts", "labelSingular": "Post", "supports": ["drafts", "revisions", "preview"], "fields": [ { "slug": "title", "type": "string", "required": true }, { "slug": "content", "type": "portableText" }, { "slug": "featuredImage", "type": "image" } ] } ], "taxonomies": [{ "name": "category", "label": "카테고리", "hierarchical": true }], "menus": [{ "name": "primary", "label": "기본 내비게이션" }]}시드 파일을 프로그래밍 방식으로 적용하세요:
import { applySeed, validateSeed } from "emdash/seed";import seedData from "../../concepts/.emdash/seed.json";
// 먼저 검증const { valid, errors } = validateSeed(seedData);
// 적용 (멱등성 - 재실행 안전)await applySeed(db, seedData, { includeContent: true, onConflict: "skip", // 'skip' | 'update' | 'error'});| 접근 방식 | 스키마 위치 | 런타임 수정 | 타입 안전성 |
|---|---|---|---|
| EmDash | 데이터베이스 | 예 (전체) | DB에서 생성됨 |
| WordPress | PHP 코드 + EAV | 제한적 (메타 필드) | 없음 |
| Strapi | 코드 파일 | 아니요 (재빌드 필요) | 빌드 시 생성됨 |
| Sanity | 코드 파일 | 아니요 (스키마 배포 필요) | 내장됨 |
| Directus | 데이터베이스 | 예 (전체) | DB에서 생성됨 |
EmDash는 Directus 모델을 따릅니다: 데이터베이스 우선 방식과 선택적 타입 생성을 제공합니다. 이는 원할 때 타입 안전한 개발을 지원하면서도 최대한의 유연성을 제공합니다.