reopt designreopt design
DocsExploreToolsPricingBuilder
Start
Start
Playground
Core Concepts
Core Concepts
Editor
Editor 개요
Block Types
Streaming
Markdown Conversion
Build & Operate
Skills
App Composition Guide
AI Integration
Authoring Playbook
Custom Blocks
Release Notes
Oopt-editor
reopt designreopt design

A design system for the AI era

  • Docs
  • Pricing
  • Releases
  • GitHub
  • Terms of Service
  • Privacy Policy

© 2026 reopt-ai. All rights reserved.

Core Concepts
  1. Docs
  2. /
  3. Core Concepts
  4. /
  5. Core Concepts

핵심 개념

Flat Spec 아키텍처, 블록 시스템, 인라인 마크, EditorStore, 카탈로그 구조

reopt design · Updated Jun 26, 2026

시작하기Playground핵심 개념개요블록 타입스트리밍Markdown 변환Skills앱 조합 가이드AI 연동Authoring 플레이북커스텀 블록릴리즈 노트

1. Flat Spec 아키텍처

opt-editor의 문서 모델은 EditorSpec이라는 flat map 구조입니다. 두 가지 핵심 필드로 구성됩니다:

  • root: string[] — 최상위 요소의 표시 순서
  • elements: Record<string, EditorElement> — 모든 요소를 ID로 매핑한 flat map
tsx
/** 문서의 단일 요소. */
interface EditorElement {
  id: string;
  type: string;
  props: Record<string, unknown>;
  children?: string[];   // 컨테이너 블록의 자식 ID 배열
  visible?: VisibilityCondition;  // 조건부 표시
  on?: Record<string, EventBinding>;  // 이벤트 바인딩
}

/** 문서 전체 스펙. */
interface EditorSpec {
  root: string[];                        // 표시 순서
  elements: Record<string, EditorElement>; // flat map
  version: number;                       // 낙관적 동시성 버전
}

트리가 아닌 flat map을 선택한 이유

  • RFC 6902 JSON Patch와 자연스럽게 호환 (/elements/p1/props/text로 직접 접근)
  • AI 스트리밍 시 요소별 독립 추가 가능 — 인덱스 시프트 없음
  • O(1) 요소 접근 — ID로 바로 조회
  • toTree() / fromTree()로 트리 변환 가능
tsx
import { toTree, fromTree } from "@reopt-ai/opt-editor";

// Flat spec → 트리 변환 (렌더링용)
const tree: BlockNode[] = toTree(spec);
// tree[0] = { id: "h1", type: "heading", props: {...}, children: [] }

// 트리 → Flat spec 복원
const restored: EditorSpec = fromTree(tree);

2. 블록 시스템

모든 콘텐츠는 블록 단위로 구성됩니다. 각 블록 타입은 BlockDefinition으로 정의되며, 타입 이름, attrs 스키마, rich-text content 여부, 렌더링 컴포넌트, AI 프롬프트 설명을 포함합니다.

tsx
interface BlockDefinition<Props extends object = object> {
  type: string;
  attrsSchema: Record<string, SchemaFieldDef>;
  contentKind?: "rich-text";
  component?: (props: BlockRenderProps<Props>) => React.ReactNode;
  canHaveChildren?: boolean;
  wrapperAs?: "div" | "li";
  skipWrapper?: boolean;
  prompt?: string;
  zodSchema?: ZodLikeSchema;
  operations?: {
    split?: (element: EditorElement, offset: number) => [EditorElement, EditorElement];
    merge?: (a: EditorElement, b: EditorElement) => EditorElement;
  };
}

기본 카탈로그에는 15개 블록이 포함됩니다. 세 가지 카테고리로 분류됩니다:

카테고리블록설명
텍스트 (5)paragraph, heading, quote, callout, coderich-text content. edit mode에서는 editableContent helper로 직접 편집
컨테이너 (5)list, list-item, table, table-row, table-cell중첩 구조. canHaveChildren: true
미디어/확장 (5)image, video, file, embed, divider미디어 임베드와 시각적 구분자

3. 인라인 마크

텍스트 prop 내에 마크다운 인라인 구문을 사용할 수 있습니다. parseInlineMarks가 텍스트를 파싱하여 MarkedSegment[]배열로 변환하고, MarkedText 컴포넌트가 렌더링합니다.

서식문법렌더링
Bold**text**<strong>
Italic*text*<em>
Code`text`<code>
Strikethrough~~text~~<s>
Link[text](url)<a>
tsx
import { parseInlineMarks } from "@reopt-ai/opt-editor";

const segments = parseInlineMarks("**bold** and *italic* text");
// [
//   { text: "bold", marks: ["bold"], attrs: undefined },
//   { text: " and ", marks: [], attrs: undefined },
//   { text: "italic", marks: ["italic"], attrs: undefined },
//   { text: " text", marks: [], attrs: undefined },
// ]

// 역변환: segments → 마크다운 문자열
import { serializeInlineMarks } from "@reopt-ai/opt-editor";

const markdown = serializeInlineMarks(segments);
// "**bold** and *italic* text"

4. EditorStore

createEditorStore가 반환하는 EditorStore는 문서 상태의 단일 소스입니다. React의 useSyncExternalStore와 호환되는 subscribe 패턴을 제공합니다.

메서드설명
getSpec()현재 flat spec 반환
getTree()트리 표현 반환 (캐시됨, 변경 시 재계산)
applyPatch(ops)RFC 6902 JSON Patch 적용 (스트림 모드)
updateElement(id, props)단일 요소 props 업데이트 (편집 모드)
insertElement(parentId, index, element)새 요소 삽입. parentId=null이면 root 레벨
removeElement(id)요소와 하위 자식 모두 제거
moveElement(id, newParentId, newIndex)요소 위치 이동
subscribe(listener)변경 구독. useSyncExternalStore 호환
undo() / redo()히스토리 탐색
getLastChangedIds()마지막 mutation에서 변경된 요소 ID (포커스 복원용)
tsx
import { createEditorStore } from "@reopt-ai/opt-editor";

const store = createEditorStore();

// 요소 삽입
store.insertElement(null, 0, {
  id: "h1",
  type: "heading",
  props: { text: "New Document", level: 1 },
});

// 요소 업데이트
store.updateElement("h1", { text: "Updated Title" });

// JSON Patch 적용 (AI 스트리밍)
store.applyPatch([
  { op: "add", path: "/elements/p1", value: { id: "p1", type: "paragraph", props: { text: "Hello" } } },
  { op: "add", path: "/root/-", value: "p1" },
]);

// Undo/Redo
store.undo();
if (store.canRedo()) store.redo();

// 변경 구독
const unsubscribe = store.subscribe(() => {
  console.log("Spec changed:", store.getSpec());
  console.log("Changed IDs:", store.getLastChangedIds());
});

5. 카탈로그

카탈로그는 사용 가능한 블록 타입의 레지스트리입니다. defineCatalog로 생성하며, 두 가지 역할을 합니다:

  1. 1. 블록 렌더링 — element type을 React 컴포넌트에 매핑
  2. 2. AI 프롬프트 생성 — 스키마를 기반으로 LLM이 이해할 수 있는 시스템 프롬프트 생성
tsx
import { defineCatalog, defaultCatalog } from "@reopt-ai/opt-editor";

// 기본 카탈로그 사용
const catalog = defaultCatalog;

// AI 시스템 프롬프트 생성
const systemPrompt = catalog.prompt();
// → "# Document Schema
## Structure
..."
// 등록된 모든 블록 타입, 스트리밍 규칙, 예제가 포함됨

// JSON Schema 생성 (Structured Output API용)
const jsonSchema = catalog.jsonSchema();
// → { $schema: "...", title: "EditorSpec", properties: {...} }

// Props 검증
const result = catalog.validateProps(spec.elements);
if (!result.valid) {
  console.error("Validation errors:", result.errors);
}

// 커스텀 카탈로그 생성
const customCatalog = defineCatalog({
  ...defaultCatalog.blocks,
  myBlock: myBlockDefinition,
});

6. 검증

validateSpec은 EditorSpec의 구조적 무결성을 검사합니다. autoFixSpec은 발견된 문제를 자동으로 수정합니다.

검증 코드심각도설명
root_missing_elementerrorroot가 존재하지 않는 요소를 참조
circular_referenceerrorchildren에 순환 참조 발견
missing_childerrorchildren이 존재하지 않는 요소를 참조
unknown_typeerror카탈로그에 없는 블록 타입 사용
id_mismatcherrorelement key와 element.id 불일치
orphan_elementwarningroot에서 도달할 수 없는 고아 요소
duplicate_rootwarningroot에 같은 ID가 중복 등록
unexpected_childrenwarning컨테이너가 아닌 블록에 children 존재
invalid_parentwarning잘못된 부모-자식 관계 (예: table-cell이 list의 자식)
tsx
import { validateSpec, autoFixSpec, diffSpecs } from "@reopt-ai/opt-editor";

// 검증
const result = validateSpec(spec, catalog);
if (!result.valid) {
  for (const issue of result.issues) {
    console.log(`[${issue.severity}] ${issue.code}: ${issue.message}`);
  }
}

// 자동 수정
const { spec: fixedSpec, fixes } = autoFixSpec(spec, catalog);
console.log("Applied fixes:", fixes);
// ["Removed orphan element "old1"", "Fixed ID mismatch: ..."]

// 두 스펙의 차이를 JSON Patch로 생성
const patches = diffSpecs(oldSpec, newSpec);
// [{ op: "replace", path: "/elements/p1/props/text", value: "Updated" }, ...]
PreviousStart패키지 설치, EditorProvider 설정, 첫 에디터 렌더링까지의 빠른 시작 가이드Start
Go to Start
NextEditor 개요AI-first 콘텐츠 에디터. 스키마 기반 스트리밍, 15개 블록 타입, Markdown 변환, 커스텀 블록 확장Editor