개요
모델은 실제 문서 데이터를 나타내고, 스키마는 유효한 구조를 정의합니다. 이들의 관계를 이해하는 것은 데이터를 변환하고, 뷰와 통합하며, 동시편집을 지원하는 견고한 에디터를 구축하는 데 필수적입니다.
이 가이드는 기본 개념을 다루고 기본 텍스트 노드부터 카드, 테이블, 인터랙티브 컴포넌트와 같은 복잡한 커스텀 스키마까지 50개 이상의 노드 타입 예제를 제공합니다.
모델 & 스키마 관계
스키마는 문서가 포함할 수 있는 것을 정의하는 계약입니다. 다음을 지정합니다:
- 어떤 노드 타입이 존재하는지
- 각 노드가 가질 수 있는 속성
- 각 노드가 포함할 수 있는 콘텐츠
- 적용할 수 있는 마크
- 검증 규칙 및 제약 조건
모델은 스키마를 준수하는 문서의 인스턴스입니다. 문서의 현재 상태를 나타내는 실제 데이터 구조입니다.
// 스키마는 규칙을 정의
const schema = {
nodes: {
paragraph: {
content: 'inline*',
group: 'block'
},
heading: {
content: 'inline*',
group: 'block',
attrs: {
level: { default: 1 }
}
}
}
};
// 모델은 스키마를 준수하는 인스턴스
const model = {
type: 'document',
children: [
{
type: 'heading',
attrs: { level: 1 },
children: [
{ type: 'text', text: '안녕하세요' }
]
},
{
type: 'paragraph',
children: [
{ type: 'text', text: '이것은 단락입니다.' }
]
}
]
};데이터 변환
데이터 변환은 문서의 다른 표현 간 변환 과정입니다:
- 직렬화: 모델 → JSON, HTML, Markdown 등
- 역직렬화: JSON, HTML, Markdown → 모델
- 마이그레이션: 스키마 변경 시 모델 구조 업데이트
- 정규화: 모델이 스키마를 준수하도록 보장
// 모델을 JSON으로 직렬화
function serializeToJSON(model) {
return JSON.stringify(model, null, 2);
}
// JSON을 모델로 역직렬화
function deserializeFromJSON(json) {
const data = JSON.parse(json);
return normalizeModel(data); // 스키마 준수 보장
}
// 모델 정규화하여 스키마 준수 보장
function normalizeModel(model) {
// 구조 검증
// 유효하지 않은 중첩 수정
// 누락된 필수 속성 추가
// 유효하지 않은 속성 제거
return validatedModel;
}
// 이전 모델을 새 스키마 버전으로 마이그레이션
function migrateModel(oldModel, oldSchema, newSchema) {
// 변경된 노드 변환
// 속성 업데이트
// 제거된 노드 타입 처리
return migratedModel;
}뷰 연동
뷰 레이어는 모델을 렌더링하고 사용자 상호작용을 처리합니다. 연동에는 다음이 필요합니다:
- 렌더링: 모델을 DOM으로 변환
- 입력 처리: DOM 변경을 모델 업데이트로 변환
- 선택 매핑: DOM 선택과 모델 위치 간 변환
- 변경 감지: 모델 변경 감지 및 뷰 업데이트
// 모델을 DOM으로 렌더링
function renderModel(model, container) {
container.innerHTML = '';
model.children.forEach(child => {
const element = renderNode(child);
container.appendChild(element);
});
}
function renderNode(node) {
switch (node.type) {
case 'paragraph':
const p = document.createElement('p');
node.children.forEach(child => {
p.appendChild(renderNode(child));
});
return p;
case 'text':
const text = document.createTextNode(node.text);
// 마크 적용
if (node.marks && node.marks.length > 0) {
return wrapWithMarks(text, node.marks);
}
return text;
// ... 기타 노드 타입
}
}
// DOM 변경 처리 및 모델 업데이트
function handleInput(domElement, model) {
// DOM 변경을 모델 작업으로 변환
const operations = diffDOMToModel(domElement, model);
operations.forEach(op => {
applyOperation(model, op);
});
return model;
}
// DOM 선택을 모델 위치로 매핑
function getModelPosition(domSelection) {
const range = domSelection.getRangeAt(0);
const path = getPathFromDOMNode(range.startContainer);
return {
path: path,
offset: range.startOffset
};
}동시편집
동시편집은 동시 편집을 처리하기 위해 작업을 변환하는 것이 필요합니다:
- 작업 변환 (OT): 동시 변경을 고려하여 작업 변환
- CRDT: 최종 일관성을 위한 충돌 없는 복제 데이터 타입
- 위치 매핑: 다른 문서 버전 간 위치 변환
- 충돌 해결: 작업이 충돌할 때 처리
// 동시 변경을 고려하여 작업 변환
function transformOperation(op1, op2) {
// op1과 op2가 충돌하지 않으면 op1을 그대로 반환
if (!operationsConflict(op1, op2)) {
return op1;
}
// 그렇지 않으면 op2를 고려하여 op1 변환
if (op1.type === 'insert' && op2.type === 'insert') {
// 같은 위치에 삽입
if (op1.path === op2.path && op1.offset === op2.offset) {
// op1을 op2 뒤로 이동
return {
...op1,
offset: op1.offset + op2.content.length
};
}
}
// 더 복잡한 변환...
return transformedOp1;
}
// 모델에 작업 적용
function applyOperation(model, operation) {
switch (operation.type) {
case 'insert':
insertNode(model, operation.path, operation.node);
break;
case 'delete':
deleteNode(model, operation.path);
break;
case 'update':
updateNode(model, operation.path, operation.attrs);
break;
}
return model;
}
// 문서 버전 간 위치 매핑
function mapPosition(position, operations) {
let mappedPosition = position;
operations.forEach(op => {
mappedPosition = transformPosition(mappedPosition, op);
});
return mappedPosition;
}노드 타입
52개 이상의 노드 타입에 대한 상세한 구현 예제, 뷰 연동 노트, 그리고 일반적인 함정들을 포함한 포괄적인 가이드입니다. 각 노드 타입은 스키마, 모델 표현, HTML 매핑, 뷰 연동, 그리고 일반적인 문제들을 다루는 자체 상세 페이지를 가지고 있습니다.