콘텐츠로 이동

콘텐츠 모델

EmDash는 스키마 정의가 코드가 아닌 데이터베이스에 존재하는 데이터베이스 우선 콘텐츠 모델을 사용합니다. 이는 런타임 스키마 수정과 비개발자 친화적인 설정을 가능하게 하는 근본적인 설계 선택입니다.

Strapi나 Keystatic과 같은 전통적인 CMS는 스키마를 코드로 정의하도록 요구합니다:

// Traditional approach - schema in code
const posts = collection({
fields: {
title: text({ required: true }),
content: richText(),
},
});

EmDash는 동일한 정보를 데이터베이스 테이블에 저장합니다:

-- _emdash_collections table
INSERT 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:wordpressWordPress에서 가져옴
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는:

  1. _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 example
function 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 타입을 생성하세요:

Terminal window
# Fetch schema from database, generate types
npx 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 }>;
}

개발자 vs. 비개발자 워크플로우

섹션 제목: “개발자 vs. 비개발자 워크플로우”

개발자는 CLI를 사용할 수 있습니다:

Terminal window
# Fetch schema, generate types
npx emdash types
# 스키마를 JSON으로 내보내기
npx emdash export-seed > seed.json

비개발자는 관리자 UI만 독점적으로 사용합니다:

  1. 관리자 패널에서 콘텐츠 유형 열기
  2. 컬렉션 추가 클릭
  3. 시각적 빌더를 통해 필드 정의
  4. 즉시 콘텐츠 생성 시작

두 접근 방식 모두 동일한 기본 데이터베이스 테이블을 수정합니다.

템플릿과 내보내기는 이식 가능한 스키마 정의를 위해 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에서 생성됨
WordPressPHP 코드 + EAV제한적 (메타 필드)없음
Strapi코드 파일아니요 (재빌드 필요)빌드 시 생성됨
Sanity코드 파일아니요 (스키마 배포 필요)내장됨
Directus데이터베이스예 (전체)DB에서 생성됨

EmDash는 Directus 모델을 따릅니다: 데이터베이스 우선 방식과 선택적 타입 생성을 제공합니다. 이는 원할 때 타입 안전한 개발을 지원하면서도 최대한의 유연성을 제공합니다.