본문 바로가기
개발 Study/React

[React/NextJS] 무료 웹 에디터 TinyMCE 사용하기

by jiyoon_92 2023. 1. 26.
반응형

tinymce 로고

무료 웹 에디터 TinyMCE 사용하기

 

지난번 CKEditor로 image load까지 커버하려고 했으나 webpack 설정 및 다른 plugin을 사용할 경우 dependency가 더 복잡해지고 파일 수 및 용량도 많아져 좀 더 가벼운 TinyMCE로 대체해야 했다. (Next framework이랑 좀 충돌이 많기도...)

찾다보니 기본 Image 라는 OpenSource plugin에 input file uploader를 연결하여 blob형태로 이미지를 캐쉬에 저장하는 예제가 있었다. 좀 더 가볍고 손쉽게 사용이 가능한 아기자기하게 생긴 TinyMCE를 사용해보자.


1. TinyMCE 설치하기

https://www.tiny.cloud/docs/tinymce/6/react-zip-host/

 

Hosting the TinyMCE .zip package with the React framework | TinyMCE Documentation

The application will require further configuration before it can be deployed to a production environment. For information on configuring the application for deployment, see: Create React App - Deployment. To deploy the application to a local HTTP Server: N

www.tiny.cloud

 

1-1. TinyMCE.zip 다운로드

Cloud서비스는 사용하지 않을거기 때문에 위 링크대로, 우선 TinyMCE.zip 파일을 다운 받는다.

1-2. npm으로 TinyMCE 패키지 다운로드

npm install --save @tinymce/tinymce-react

1-3. TinyMCE.zip 압축 풀기

압축 푸는 경로

global.css가 들어있는 public 폴더에 tinymce 폴더를 생성한 후, 그 안에 압축을 푼다.

1-4. LanguagePack 다운로드

Language Pack을 다운로드한 후 압축을 푼 tinymce/langs 폴더에 저장한다. (ex. 한국어 - ko_KR.js)

https://www.tiny.cloud/get-tiny/language-packages/

 

Language Packages | Trusted Rich Text Editor | TinyMCE

TinyMCE Language Packages

www.tiny.cloud


2. Editor 구현하기

Image 플러그인 document를 보고 아래와 같이 구현했다.

https://www.tiny.cloud/docs/tinymce/6/image/

 

Image plugin | TinyMCE Documentation

tinymce.init({ selector: 'textarea#file-picker', plugins: 'image code', toolbar: 'undo redo | link image | code', /* enable title field in the Image dialog*/ image_title: true, /* enable automatic uploads of images represented by blob or data URIs*/ automa

www.tiny.cloud

반응형
import React, { useEffect, useRef, useState } from 'react';
import { Editor } from '@tinymce/tinymce-react';

type Props = {
  editorId : string //Editor 구분 ID
  defaultText : string //기본문구
  heightValue : number //Editor 높이
}
const WebEditor = ({editorId, defaultText, heightValue} : Props) : JSX.Element => {
  const editorRef = useRef(null);
  const [editorHeight, setEditorHeight] = useState(heightValue);
  
  //이미지 로드 시 callback
  const loadHandler = (callback, e) => {
    const file = e.target.files[0];
        
    const reader = new FileReader();
    reader.addEventListener('load', () => {
      /*
        Note: Now we need to register the blob in TinyMCEs image blob
        registry. In the next release this part hopefully won't be
        necessary, as we are looking to handle it internally.
      */
      const id = 'blobid' + (new Date()).getTime();
      const blobCache =  editorRef.current.activeEditor.editorUpload.blobCache;
      const base64 = String(reader.result).split(',')[1];
      const blobInfo = blobCache.create(id, file, base64);
      blobCache.add(blobInfo);

      /* call the callback and populate the Title field with the file name */
      callback(blobInfo.blobUri(), { title: file.name });
    });
    reader.readAsDataURL(file);
  }
  useEffect(()=>{
    setEditorHeight(heightValue);
  }, [heightValue])
  return (
    <>
      <Editor
        id = {editorId}
        tinymceScriptSrc={process.env.NEXT_PUBLIC_API_URL + '/tinymce/tinymce.min.js'}
        onInit={(evt, editor) => editorRef.current = editor}
        initialValue={defaultText}
        init={{
          language: 'ko_KR',
          height: editorHeight,
          menubar: false,
          plugins: [
            'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
            'anchor', 'searchreplace', 'visualblocks', 'code',
            'insertdatetime', 'media', 'table', 'help', 'wordcount', 'autosave'
          ],
          toolbar1: 'blocks alignleft aligncenter alignright alignjustify bold italic forecolor bullist numlist outdent indent',
          toolbar2: 'table link image media code restoredraft removeformat help | undo redo ',
          content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }',
          autosave_interval: '10s',
          image_title: true,
          file_picker_types: 'image',
          images_file_types: 'jpg',
          file_picker_callback: (cb, value, meta) => {
            const input = document.createElement('input');
            input.setAttribute('type', 'file');
            input.setAttribute('accept', 'image/jpeg');
        
            input.addEventListener('change', (e) => {
              loadHandler(cb, e);
            });
            input.click();
          },
          media_live_embeds: true,
          iframe_template_callback: (data) => {
    `<iframe title="${data.title}" width="${data.width}" height="${data.height}" src="${data.source}"></iframe>`
          }
        }}
      />
    </>
  );
}

export default WebEditor;
  • file_picker_types : 파일픽커가 받아들일 파일 타입
  • file_picker_callback : 파일픽커 호출 시 로드되는 함수
  • loadHandler() : 이미지 로드시 호출되는 함수로 이미지를 Blob형태로 Editor에서 제공하는 캐쉬에 저장한다
  • image_file_types : 이미지 로드할 input에서 제공되는 image extension

위 정도만 알고있으면 이미지 업로드는 문제가 아니다. 다만, 업로드 후 저장 시 서버에 어떤식으로 저장할 지는 좀 더 고민해봐야겠다.

주목할 점은 onInit()에서 useRef를 이용해 Editor를 this처럼 받아 컴포넌트 내 다른함수에서도 활용가능하도록 하는 것. 오늘도 React를 이렇게 한발짝 알아간다.

이런 훅은 대체 언제쓰지 하는 것들도 쓰다보면? 어떤 상황에 적절하게 사용할 수 있는지 감이 생기는 것 같다.

 

반응형

댓글