텍스트 노드 타입

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

스키마 정의

텍스트 노드 타입의 스키마 정의:

{
  text: {
    group: 'inline',
    // 텍스트 노드는 리프 노드, 자식 없음
  }
}

모델 표현

모델 표현 예제:

{
  type: 'text',
  text: '안녕하세요',
  marks: [
    { type: 'bold' },
    { type: 'italic' }
  ]
}

HTML 직렬화

모델을 HTML로 변환:

function serializeText(node) {
  let html = escapeHtml(node.text);
  
  // 순서대로 마크 적용
  if (node.marks && node.marks.length > 0) {
    node.marks.forEach(mark => {
      html = wrapWithMark(html, mark);
    });
  }
  
  return html;
}

function wrapWithMark(html, mark) {
  const tagMap = {
    bold: 'strong',
    italic: 'em',
    underline: 'u',
    code: 'code'
  };
  const tag = tagMap[mark.type];
  return tag ? '<' + tag + '>' + html + '</' + tag + '>' : html;
}

HTML 역직렬화

HTML을 모델로 파싱:

function parseText(domNode) {
  return {
    type: 'text',
    text: domNode.textContent,
    marks: extractMarks(domNode)
  };
}

function extractMarks(textNode) {
  const marks = [];
  let current = textNode.parentElement;
  
  while (current && current !== editor) {
    const mark = getMarkFromElement(current);
    if (mark) marks.push(mark);
    current = current.parentElement;
  }
  
  return marks;
}

뷰 연동

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

뷰 연동 코드:

// 렌더링
const textNode = document.createTextNode(node.text);
// 마크는 직렬화 중 부모 요소로 래핑하여 적용됨

// 텍스트 노드 편집
textNode.addEventListener('input', (e) => {
  // 텍스트 콘텐츠 업데이트
  const newText = textNode.textContent;
  updateTextNode(node, newText);
});

// 텍스트 노드의 선택
function getTextPosition(textNode, offset) {
  return {
    path: getPathToNode(textNode),
    offset: offset
  };
}

일반적인 문제

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

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

// 문제: 텍스트 노드는 블록 또는 인라인 노드 내부에 있어야 함
// 해결: 부모 노드 타입 검증
function validateTextNode(textNode) {
  const parent = textNode.parentNode;
  if (!isBlockNode(parent) && !isInlineNode(parent)) {
    // 단락으로 래핑
    wrapInParagraph(textNode);
  }
}

// 문제: 빈 텍스트 노드
// 해결: 빈 텍스트 노드 제거 또는 병합
function normalizeTextNodes(element) {
  const walker = document.createTreeWalker(
    element,
    NodeFilter.SHOW_TEXT
  );
  const toRemove = [];
  let node;
  
  while (node = walker.nextNode()) {
    if (node.textContent.trim() === '') {
      toRemove.push(node);
    }
  }
  
  toRemove.forEach(n => n.remove());
}

// 문제: 인접한 텍스트 노드
// 해결: 인접한 텍스트 노드 병합
function mergeAdjacentTextNodes(element) {
  const walker = document.createTreeWalker(
    element,
    NodeFilter.SHOW_TEXT
  );
  let prevNode = null;
  let node;
  
  while (node = walker.nextNode()) {
    if (prevNode && prevNode.parentNode === node.parentNode) {
      prevNode.textContent += node.textContent;
      node.remove();
    } else {
      prevNode = node;
    }
  }
}

구현

완전한 구현 예제:

class TextNode {
  constructor(text, marks = []) {
    this.type = 'text';
    this.text = text;
    this.marks = marks;
  }
  
  toDOM() {
    const textNode = document.createTextNode(this.text);
    // 마크는 직렬화 중 부모에 의해 적용됨
    return textNode;
  }
  
  static fromDOM(domNode) {
    const text = domNode.textContent;
    const marks = extractMarks(domNode);
    return new TextNode(text, marks);
  }
}