문서 노드 타입

카테고리: 기본 • 뷰 연동 노트를 포함한 상세 구현 가이드

스키마 정의

문서 노드 타입의 스키마 정의:

{
  document: {
    content: 'block+',
    // 문서는 루트 노드, 항상 존재함
  }
}

모델 표현

모델 표현 예제:

{
  type: 'document',
  children: [
    {
      type: 'paragraph',
      children: [{ type: 'text', text: '안녕하세요' }]
    }
  ]
}

HTML 직렬화

모델을 HTML로 변환:

function serializeDocument(node) {
  return serializeChildren(node.children);
}

// 문서 자체는 래퍼 요소를 생성하지 않음
// 모든 최상위 블록의 컨테이너임

HTML 역직렬화

HTML을 모델로 파싱:

function parseDocument(html) {
  const parser = new DOMParser();
  const doc = parser.parseFromString(html, 'text/html');
  
  return {
    type: 'document',
    children: Array.from(doc.body.childNodes)
      .map(node => parseNode(node))
      .filter(Boolean)
  };
}

뷰 연동

뷰 연동 노트: 이 노드 타입을 뷰 레이어에서 구현할 때 contenteditable 동작, 선택 처리, 이벤트 관리에 특히 주의하세요.

뷰 연동 코드:

// 렌더링
const container = document.createElement('div');
container.contentEditable = 'true';
node.children.forEach(child => {
  container.appendChild(renderNode(child));
});

// 문서 레벨 이벤트 처리
container.addEventListener('input', handleDocumentInput);
container.addEventListener('paste', handleDocumentPaste);
container.addEventListener('keydown', handleDocumentKeydown);

// 선택 관리
function getDocumentSelection() {
  const selection = window.getSelection();
  if (!selection.rangeCount) return null;
  return getModelPosition(selection);
}

일반적인 문제

일반적인 함정: 이 노드 타입을 구현할 때 자주 발생하는 문제들입니다. 구현 전에 주의 깊게 검토하세요.

일반적인 문제 및 해결 방법:

// 문제: 문서는 항상 최소 하나의 블록을 가져야 함
// 해결: 문서가 항상 콘텐츠를 가지도록 보장
if (node.children.length === 0) {
  node.children.push({
    type: 'paragraph',
    children: []
  });
}

// 문제: 문서 구조 검증
// 해결: 모든 자식이 블록 노드인지 검증
function validateDocument(doc) {
  return doc.children.every(child => 
    isBlockNode(child)
  );
}

// 문제: 빈 문서 처리
// 해결: 항상 최소 하나의 빈 단락 유지
function ensureDocumentNotEmpty(doc) {
  if (doc.children.length === 0) {
    doc.children.push(createEmptyParagraph());
  }
}

구현

완전한 구현 예제:

class DocumentNode {
  constructor(children) {
    this.type = 'document';
    this.children = children || [];
  }
  
  toDOM() {
    const fragment = document.createDocumentFragment();
    this.children.forEach(child => {
      fragment.appendChild(child.toDOM());
    });
    return fragment;
  }
  
  static fromDOM(domNode) {
    const children = Array.from(domNode.childNodes)
      .map(node => parseNode(node))
      .filter(Boolean);
    return new DocumentNode(children);
  }
}