본문 바로가기
개발적인

CKEditor에서 커스텀 버튼 및 기능 추가하는 방법

by klm hyeon woo 2024. 9. 23.

목적

· 배경

· 커스텀 버튼 및 기능 추가하기


배경

본래 에디터 프로덕트를 개발을 했던 적은 있지만, 파일 업로드를 초기부터 개발을 한 적은 없기 때문에 기록을 위해 포스팅을 진행합니다. 백오피스 개발을 진행하던 도중 CKEditor에서 본문 파일 업로드 기능을 만들어야했습니다. CKEditor에서는 쉽게 파일 업로드 기능을 구현하기 위해서는 프리미엄 플랜을 사용해야하는데, 굳이 프리미엄을 사용하지 않고 그 외에 기능들은 자체 기능으로 구현이 되어있었기 때문에 파일 업로드 기능 또한 직접 구현을 해야했습니다. 이때 프리미엄 플랜을 사용하지 않고 구현을 할 수도 있는데, 이를 위해 커스텀 플러그인과 버튼을 구성하여 에디터에 추가하는 과정을 진행해야합니다. CKEditor에서 제시하는 업로드에 대한 방식은 아래와 같습니다.

 

[1] Easy Image : Image를 에디터의 자체 클라우드에 업로드 하는 방식 (자체 서버가 아닌 CK 클라우드에 업로드하는 방식)

[2] CKFinder : 이미지 및 파일 업로드가 모두 가능하며, 관리까지 가능한 플러그인 (정말 좋은 방법이지만, 유료 플랜제입니다)

[3] Base64 Adapter : 이미지를 Base64-encoded 문자열로 변환하여 저장하는 방식 (이미지 업로드를 하는 방식에 많이 사용되기도 하며, 파일 관리 없이 문자열을 DB에 저장할 수 있어 편하다는 장점이 있음)

[4] Simple Upload Adapter : XMLHttpRequest API를 이용하여 파일 및 이미지를 서버에 업로드하고 응답 값을 통해 처리하는 커스텀 어댑터 방식

 

이미 서비스에 이미지 기능은 추가가 되어있었으며, 서비스에서 제시하는 요구사항은 파일 업로드 기능이었기 때문에 파일 업로드를 구현할 수 있는 방안에 대해 생각을 해야했습니다. 툴팁과 원하는 아이콘, 그리고 요구사항에 맞는 동작방식으로 구현을 해야하기에 로직을 새로 구성하는 방향성으로 진행을 했습니다.

커스텀 버튼 및 기능 추가하기

CKEditor 공식 문서에는 클래스 문법을 통한 예제가 많으며, 커스텀 플러그인에 대한 예제는 클래스 문법으로 구현이 되어있기 때문에 공식 문서에서 제시하는 방향대로 클래스 문법을 사용하였습니다. 다만 여기서 문제는, 기존에 이미지 업로드 어댑터 방식이 에디터의 JS 파일 안에 클래스 문법 형태의 커스텀 어댑터 형식으로 구현이 되어있었습니다. 한 파일에서 다중 클래스를 구성할 수 없기 때문에 클래스 파일을 분할하여 임포트 방식을 통해 사용하는 방식으로 채택을 하였습니다. 

 

그래서 기존 클래스 문법으로 구성이 되어있는 이미지 어댑터 플러그인을 별도의 파일로 생성하고, 추가적으로 파일 업로드 플러그인을 클래스 문법으로 새롭게 구성을 하였습니다. 

// This is Custom Plugin File, FileUploadPlugin.js
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
import { editorApi } from '@/api';
 
export default class FileUploadPlugin {
  constructor(editor) {
    this.editor = editor;
 
    editor.ui.componentFactory.add('fileUpload', (locale) => {
      const view = new ButtonView(locale);
      const FILE_MAX_SIZE = 10 * 1024 * 1024;
 
      view.set({
        label: 'Insert file',
        tooltip: true,
        icon: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#333">'
          + '<path d="M240-80q-33 0-56.5-23.5T160-160v-640q0-33 23.5-56.5T240-880h360l200 200v520q0 33-23.5 56.5T720-80H240Zm0-80h480v-480H560v-160H240v640Zm240-40q67 0 113.5-47T640-360v-160h-80v160q0 33-23 56.5T480-280q-33 0-56.5-23.5T400-360v-220q0-9 6-14.5t14-5.5q9 0 14.5 5.5T440-580v220h80v-220q0-42-29-71t-71-29q-42 0-71 29t-29 71v220q0 66 47 113t113 47ZM240-800v160-160 640-640Z"/>'
          + '</svg>',
      });
 
      view.on('execute', () => {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = '*';
        input.click();
 
        input.onchange = () => {
          const file = input.files[0];
          if (file) {
            if (file.size > FILE_MAX_SIZE) {
              alert('최대 파일 크기(10MB)를 초과했습니다.');
            } else {
              // NOTE: Need Data Fetching Logic
              editorApi.uploadFile('test');
              const formData = new FormData();
              formData.append('file', file);
              // NOTE: Need Modify Fetching Response File URL
              const fileUrl = 'https://asec.ahnlab.com';
              editor.model.change((writer) => {
                const sel = editor.model.document.selection;
                let fileName = '';
                if (sel.isCollapsed) {
                  fileName = file.name;
                } else {
                  const range = sel.getFirstRange();
                  const selectedText = Array.from(range.getItems())
                    .reduce((acc, cur) => {
                      acc += cur.data;
                      return acc;
                    }, '');
                  fileName = selectedText;
                }
                const linkElement = writer.createText(fileName);
                writer.setAttribute('linkHref', fileUrl, linkElement);
                editor.model.insertContent(linkElement, sel);
              });
            }
          }
        };
      });
 
      return view;
    });
  }
}

기존 CKEditor에서는 파일 업로드 기능을 유료 플랜제 이외에는 제공을 하고 있지 않기 때문에, 툴바에 아무리 파일 관련 이름을 기재해도 추가가 되지 않습니다. 위의 코드에서 주의깊게 바라봐야하는 점은 componentFactoryfileUpload를 커스텀하게 추가해줌으로서 아래 코드에서 툴바 추가를 위해 선언한 내용들에 FileUploadPluginplugins 배열 안으로 삽입되었을 때 fileUpload를 툴바에 명시해줌으서 커스텀하게 설정한 버튼이 에디터에 활성화되는 것을 확인할 수 있습니다.

// This is Editor File
editorConfig: {
      plugins: [
        Essentials,
        Bold,
        Italic,
        Underline,
        Strikethrough,
        Subscript,
        Superscript,
        Code,
        Link,
        Paragraph,
        Font,
        Indent,
        IndentBlock,
        Heading,
        ParagraphButtonUI,
        CodeBlock,
        Highlight,
        HorizontalLine,
        EasyImage,
        Image,
        ImageToolbar,
        ImageStyle,
        ImageResize,
        Table,
        TableToolbar,
        Alignment,
        List,
        BlockQuote,
        FileRepository,
        HtmlEmbed,
        MediaEmbed,
        FileUploadPlugin,
      ],
      toolbar: {
        shouldNotGroupWhenFull: true,
        viewportTopOffset: 60,
        items: [
          'heading', '|', 'bold', 'italic', 'underline', 'strikethrough', 'subscript', 'superscript', 'blockQuote', 'link', 'horizontalLine', '|',
          'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor', 'highlight', '|', 'alignment:justify', 'alignment:left', 'alignment:center', 'alignment:right', '|',
          'outdent', 'indent', '|', 'bulletedList', 'numberedList', '|', 'code', 'codeBlock', '|', 'htmlEmbed', '|', 'imageUpload', 'fileUpload', 'mediaEmbed', 'insertTable', '|', 'undo', 'redo',
        ],
      },
 
...

실제 에디터 설정에는 플러그인을 먼저 추가해주고, 관련하여 fileUpload를 추가해줌으로서 정상적으로 에디터에 파일 업로드 버튼이 렌더링 되는 것을 확인할 수 있습니다. 위의 코드들은 예시에 대한 코드이며 위의 코드를 베이스로 다양한 커스텀 플러그인 및 버튼을 자유롭게 추가할 수 있습니다. 또한 이벤트 메서드를 통해 사용자가 에디터에 커스텀한 동작을 추가할 수 있습니다.

 

댓글