🎊 TOAST UI Editor 3.0 🎊


TOAST UI Editor 2.0을 출시하며 에디터에는 마크다운 파싱의 정확도와 성능 향상, 구문 강조 기능 추가, 스크롤 싱크의 정확도 향상 등 괄목할 만한 개선 사항들이 있었습니다. 그리고 이러한 성과는 ToastMark라는 자체적인 마크다운 파서를 도입함으로써 이뤄낼 수 있었습니다. 하지만 아쉽게도 2.0에서는 마크다운 에디터에 초점을 맞춰 작업을 진행하였기 때문에 위지윅 에디터와 에디터를 구성하는 UI(툴바나 스위치 탭)에 대한 기능 추가나 개선은 없었습니다.

TOAST UI Editor 3.0에서는 마크다운에 국한된 것이 아닌 에디터의 전반적인 구조와 사용성 개선에 초점을 맞춰 작업을 진행했습니다. 마크다운 커스텀 문법 지원, 위젯 노드 삽입, 플러그인 시스템 개선처럼 사용자가 에디터의 기능을 쉽게 확장할 수 있는 방법들이 추가되었고, 디자인도 세련되고 이쁘게 개편되었습니다. 🙌

그럼 TOAST UI Editor 3.0에는 어떠한 변화가 있는지 살펴보겠습니다.

🧐 3.0 어떠한 변화가 있을까요?

코어 모듈 교체 ➔ 더 가벼운 에디터

기존의 마크다운 에디터는 CodeMirror와 ToastMark가 각각 텍스트 정보를 관리하며 변경 정보를 서로 동기화했습니다. 반면, 위지윅 에디터는 squire를 사용하여 콘텐츠를 편집하고 관리했습니다. 즉, 각 에디터에서 데이터를 변경하고 관리하는 방식과 이를 제어하는 클래스의 구조가 서로 완전히 달랐기 때문에 아래처럼 내부적인 코드 통일성이나 기능 확장에 많은 제약이 있었습니다.

  • 특정 텍스트를 추가하거나 변경할 때 유사한 기능임에도 불구하고 내부적으로 서로 다른 API와 옵션을 이용하게 됩니다.
  • heading, list, table 등 동일한 노드를 내부적으로 관리하지만, 두 에디터가 완전히 다른 객체 형태로 노드를 관리하기 때문에 코드를 파악하기 어렵습니다.

    물론 마크다운은 텍스트 기반의 에디터이고, 위지윅 에디터는 DOM 노드를 기반으로 한 편집 도구이기 때문에 당연히 데이터 모델의 구조가 다릅니다. 하지만 두 에디터가 하나의 공통된 모듈을 사용하여 노드 데이터를 관리한다면, 데이터를 추상화하고 관리하는 클래스나 오퍼레이션의 구조는 공통으로 사용할 수 있습니다.

  • 사용자가 옵션이나 API를 사용하여 커스터마이징하려는 경우 굉장히 번거롭습니다. 만약 특정 노드의 렌더링 결과를 커스터마이징하고 싶다면, 마크다운 에디터, 위지윅 에디터의 각각의 구조에 맞게 직접 수정해야 합니다.

이를 해결하기 위해, TOAST UI Editor 3.0에서는 Prosemirror(위지윅 에디터 제작을 위한 개발 도구)를 사용하여 각 에디터의 내부 의존성 모듈을 하나로 통일했습니다. 그 덕에 내부 구조를 동일하게 가져갈 수 있었고, 기존에 사용하던 CodeMirror, squire, to-mark 의존성을 모두 제거할 수 있었습니다.

// 에디터 내의 모든 노드는 아래와 같은 클래스 구조를 가집니다.
export class ListItem extends NodeSchema {
  get name() {
    return 'listItem';
  }

  get schema() {
    return {
      content: 'paragraph listGroup*',
      attrs: {
        task: { default: false },
        checked: { default: false },
        rawHTML: { default: null },
      },
      defining: true,
      parseDOM: [
        // ...
      ],
      toDOM({ attrs }: ProsemirrorNode): DOMOutputSpecArray {
        // ...
      },
    };
  }

  commands(): EditorCommand {
    // ...
  }

  keymaps() {
    return {
      Enter: this.commands()(),
    };
  }
}

또한 전체 번들 파일의 용량도 602.1KB에서 495.6KB로 약 30% 정도 경량화할 수 있었습니다.

images

Prosemirror를 선택하게 된 배경과 이유에 대해서는 별도의 글에서 다루겠습니다.

3.0 버전에서는 ESM 번들도 제공하기 때문에 레거시 브라우저 지원이 필요없다면 ESM 번들을 사용하여 더 효율적으로 에디터를 사용할 수 있습니다.

마크다운 커스텀 문법 지원

TOAST UI Editor는 기본적으로 CommonMark 스펙을 준수하며, 추가로 GFM 스펙도 지원합니다. 하지만, 만약 수식을 표현하거나 차트 같은 요소를 렌더링하고 싶다면 어떨까요? TOAST UI Editor 3.0에서는 이러한 사용성을 위해 사용자만의 커스텀 문법을 정의할 수 있는 옵션을 제공합니다.

커스텀 문법을 사용하면 다음 예시처럼 KaTeX와 같은 문법을 사용하여 수식을 표현할 수 있습니다.

마크다운 image

위지윅 image

위의 이미지에서 볼 수 있듯이 $$ 기호로 감싸진 블록 내에 텍스트를 입력하여 커스텀 문법을 사용할 수 있습니다. 커스텀 문법을 사용하여 마크다운에서 지원하지 않는 문법도 적절한 파싱 로직을 직접 정의하여 렌더링할 수 있습니다.

위젯 노드

TOAST UI Editor 3.0에서는 일반 텍스트를 특정한 위젯 노드로 보여줄 수 있는 widgetRules 옵션이 추가되었습니다. 이 옵션을 사용하면 링크 텍스트를 멘션 노드로 보여주거나 텍스트를 원하는 형태의 DOM 노드로 보여줄 수 있습니다.

const reWidgetRule = /\[(@\S+)\]\((\S+)\)/;

const editor = new Editor({
  el: document.querySelector('#editor'),
  widgetRules: [
    {
      rule: reWidgetRule,
      toDOM(text) {
        const rule = reWidgetRule;
        const matched = text.match(rule);
        const span = document.createElement('span');
  
        span.innerHTML = `<a class="widget-anchor" href="${matched[2]}">${matched[1]}</a>`;
        return span;
      },
    },
  ],
});

예제 코드에서 볼 수 있듯이 widgetRules는 배열 형태로 각각의 규칙을 정의하며, 각 규칙은 rule, toDOM이라는 프로퍼티로 구성됩니다.

  • rule: 반드시 정규식 값이여야하며, 이 정규식에 맞는 텍스트는 위젯 노드로 치환되어 렌더링 됩니다.
  • toDOM: 렌더링 될 위젯 노드의 DOM 노드를 정의합니다.

image

또한 아래 이미지처럼 팝업 위젯과 연동하여 위젯 노드를 삽입할 수도 있습니다.

image

플러그인 시스템

TOAST UI Editor에서는 5개의 플러그인을 기본으로 제공합니다.

플러그인 설명
chart 차트를 렌더링하기 위한 플러그인
code-syntax-highlight 코드 하이라이팅을 위한 플러그인
color-syntax 컬러피커 사용을 위한 플러그인
table-merged-cell 병합 테이블 셀을 사용하기 위한 플러그인
uml UML 사용을 위한 플러그인

5개의 기본 플러그인 외에도 사용자가 직접 플러그인 함수를 정의하여 사용할 수도 있습니다. 기존의 2.0버전에서는 플러그인을 정의하기 위한 명확한 포맷이 없었고 아래처럼 직접 에디터 인스턴스에 접근하여 작업했습니다. 기존의 플러그인 정의 방식은 에디터와 플러그인 간의 강한 결합도를 만들고 코드를 파악하기 어렵게 만들었습니다.

v2.0

export default function colorSyntaxPlugin(editor, options = {}) {
  // ...
  editor.eventManager.listen('convertorAfterMarkdownToHtmlConverted', html => {
    // ...
  });

  editor.eventManager.listen('convertorAfterHtmlToMarkdownConverted', markdown => {
    // ...
  });

  if (!editor.isViewer() && editor.getUI().name === 'default') {
    editor.addCommand('markdown', {
      name: 'color',
      exec(mde, color) {
        // CodeMirror 인스턴스에 접근
        const cm = mde.getEditor();
        const rangeFrom = cm.getCursor('from');
        // ...
      }
    });

    editor.addCommand('wysiwyg', {
      name: 'color',
      exec(wwe, color) {
        if (!color) {
          return;
        }

        // squire 인스턴스에 접근
        const sq = wwe.getEditor();
        const tableSelectionManager = wwe.componentManager.getManager('tableSelection');

        // ...
      }
    });
  }
});

위의 코드는 2.0 버전의 color-syntax 플러그인 코드 일부입니다. 정해진 포맷없이 에디터 인스턴스 API에 종속적인 코드가 많으며, 심지어 CodeMirror와 squire에 직접 접근하여 내부 상태를 조작하는 코드들도 있습니다.

TOAST UI Editor 3.0에서는 기존 구조를 모두 제거하고 정해진 포맷으로 플러그인을 정의할 수 있도록 변경했습니다. 또한 플러그인 측에서 에디터에 대한 최소한의 정보만 알고 동작하도록 분리했습니다.

v3.0

export default function colorSyntaxPlugin(context, options) {
  // ...
  return {
    markdownCommands: {
      color: ({ selectedColor }, { tr, selection, schema }, dispatch) => {
        if (selectedColor) {
          // ...
          return true;
        }
        return false;
      },
    },
    wysiwygCommands: {
      color: ({ selectedColor }, { tr, selection, schema }, dispatch) => {
        if (selectedColor) {
          // ...
          return true;
        }
        return false;
      },
    },
    toolbarItems: [
      {
        groupIndex: 0,
        itemIndex: 3,
        item: toolbarItem,
      },
    ],
    // ...
  };
}

새로 변경된 color-syntax 플러그인의 코드는 훨씬 간단하고 읽기 쉽습니다. 플러그인 함수에서 반환하는 객체의 프로퍼티(markdownCommands, wysiwygCommands, toolbarItems)로 어떠한 동작을 제어하는지 명확하게 알 수 있습니다. 또한 이 과정에서 각각의 프로퍼티는 에디터의 API나 프로퍼티에 종속적이지 않습니다.

개편된 플러그인 시스템에서는 3.0버전에서 추가된 마크다운 커스텀 문법을 주입하거나 커맨드를 등록하는 작업이 훨씬 간결해졌습니다. 현재 에디터는 5개의 플러그인을 기본으로 제공하지만, 추후 summary, details태그를 위한 플러그인이나 자동 완성 팝업과 같은 플러그인을 추가할 예정입니다.

개편된 플러그인 시스템을 적용하고 싶다면 먼저 마이그레이션 가이드를 참조해주세요!

디자인

TOAST UI Editor 3.0에서는 디자인도 완전히 변경되었습니다. 툴바나 탭과 같은 전반적인 UI 요소의 크기가 커져 가독성이 좋아졌고, 외곽을 굴곡지게 처리하여 더 부드러운 느낌을 주었습니다.

v2.0

image

v3.0

image

또한 다크 테마가 추가되었습니다.

// ...
import '@toast-ui/editor/dist/toastui-editor.css';
import '@toast-ui/editor/dist/theme/toastui-editor-dark.css';

const editor = new Editor({
  // ...
  theme: 'dark'
});

image

📝 마이그레이션 가이드

TOAST UI Editor 3.0에서는 위에서 언급한 기능외에도 대대적인 변화가 있었습니다. 그만큼 사용 방법에도 큰 변화가 생겼기 때문에, 마이그레이션 가이드를 준비했습니다.

영어(🇺🇸): https://github.com/nhn/tui.editor/blob/master/docs/v3.0-migration-guide.md 한국어(🇰🇷): https://github.com/nhn/tui.editor/blob/master/docs/v3.0-migration-guide-ko.md

또한 위에서 언급한 기능들은 아래 가이드에서 자세한 내용을 볼 수 있습니다.

✍ 이외에 변경 사항

신규 기능 외에도 아래처럼 내부 유지 보수를 위한 다양한 변화가 있었습니다.

  • 타입스크립트 적용: 내부 코드를 모두 타입스크립트를 변경했습니다. 정적 타입 체크를 통한 미연의 오류 방지, 타입 추론의 장점을 얻었고 견고한 코드를 작성할 수 있었습니다.
  • virtual DOM 적용: 자체적으로 아주 작은 virtual DOM을 만들었고, 기존 명령형 기반의 DOM 조작 코드를 선언적인 형태로 변경했습니다. 이미 메인테이너가 ReactVue.js 같은 라이브러리에 익숙하였기 때문에 변경된 구조를 사용해 쉽게 개발할 수 있었고, UI 컴포넌트와 관련된 코드도 약 40%정도 줄일 수 있었습니다.
  • 모던 개발도구 적용: snowpack과 같은 no-bundling 철학을 가진 개발 도구를 사용하여, 개발 생산성을 굉장히 향상시켰습니다. 또한 npm7workspace를 통해 모노레포 패키지 간의 의존성을 더 효율적으로 관리했습니다.

🚀 앞으로의 계획은?

TOAST UI Editor의 가장 큰 특징은 마크다운을 기반으로 한 에디터이지만 위지윅 에디터를 동시에 지원한다는 점입니다. TOAST UI Editor 3.0에서는 이러한 장점을 더욱 강화하고자 두 에디터 간의 구조적인 통일과 호환성을 높이는 데 많은 노력을 하였습니다. 그 결과, 기능 확장을 위해 기존에 존재했던 많은 제약 사항들을 없앨 수 있었습니다.

앞으로의 업데이트에서는 다음과 같은 사항들을 계획하고 있습니다.💪

  • 플러그인 생태계 확장
  • SSR(Server Side Rendering) 지원
  • 동시 편집 기능 지원

기존에는 불가능했던 동시 편집 기능을 R&D할 예정이고, 기본 제공 플러그인과 커스터마이징 옵션도 더 확장할 것입니다. 이외에도 두 에디터간의 호환성을 더 강화하고, 긴 문서를 편집할 때의 성능을 향상시킬 예정입니다.

TOAST UI Editor의 Github은 언제나 열려 있습니다! 앞으로의 업데이트도 많이 기대해주세요. 😀