모델 & 스키마

모델과 스키마의 관계, 데이터 변환, 뷰 연동, 동시편집에 대한 심화 가이드 및 기본 텍스트 노드부터 복잡한 커스텀 스키마까지 50개 이상의 노드 타입 예제를 제공합니다.

개요

모델은 실제 문서 데이터를 나타내고, 스키마는 유효한 구조를 정의합니다. 이들의 관계를 이해하는 것은 데이터를 변환하고, 뷰와 통합하며, 동시편집을 지원하는 견고한 에디터를 구축하는 데 필수적입니다.

이 가이드는 기본 개념을 다루고 기본 텍스트 노드부터 카드, 테이블, 인터랙티브 컴포넌트와 같은 복잡한 커스텀 스키마까지 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 매핑, 뷰 연동, 그리고 일반적인 문제들을 다루는 자체 상세 페이지를 가지고 있습니다.

모든 노드 타입 보기 →