[슬랙 클론코딩] 에디터 붙이기 1-1 | TinyMCE-react

jung moon chai·2022년 11월 9일
1

시작에 앞서서 textarea안에 이모티콘을 보여줄 목적으로 시작 하였으나, textarea안에서 어떻게 react-icons의 태그들을 보여줄 수 있을지 고민하다가, 아직 크게 아이디어가 떠오르지 않아 일단 에디터를 붙여보기로 했다.
그리고 기존 코드들에서 충돌나거나 하는 부분들이 생각보다 많아서 파트를 나누기로 했으며, 이번 포스트에서는 에디터만 붙이고, 이미지업로드 기능 api정도만 작업하는 내용을 정리하였다.

1. TinyMCE

내가 애용하는 에디터이다 위지윅 계열로 알고 있으며, 실제로 포트폴리오 게시판등에 등록해서 아주 유용하게 사용중인 에디터이다. 일단 최근 버전이 얼마나 업데이트 되었는지는 모르겠지만, 내 포트폴리오에서 사용했던 4.0.0버전을 사용 할 것이며, 마침 제로초님께서 드래그 앤 드랍 추가 보너스 기능때문에 이미지업로드 api가 있어 에디터에 이미지를 등록하는 부분까지 연결을 시도해볼 참이다.

$ npm i @tinymce/tinymce-react@4.0.0

다행히 타입이 지원되므로 추가 @types버전은 설치 하지 않아도 된다.
tiny-mce는 api키가 필요하므로 이곳에서 회원가입 후 대시보드에서 개인설정과 도메인을 등록해 주어야 한다.

등록 하게 되면 대시보드 메인 하단에 api키가 발급된다.

그럼 이제 .env에 api키를 넣고 사용하자.


2. 에디터 컴포넌트 만들기

components 루트에 기존 컴포넌트와 같은 방식으로 EditorChatBox 루트를 만들고 그 안에 index.tsx styles.tsx 파일을 생성
기존 챗박스의 코드를 그대로 복사하고 textarea 부분만 에디터로 교체하고 에디터내의 기능들을 살려볼 것이다.

2-1. 에디터 띄우기

// /components/EditorChatBox/index.tsx

import React, { FC, useCallback, useEffect, useRef } from 'react';
import autosize from 'autosize';
import gravatar from 'gravatar';
import { Mention, SuggestionDataItem } from 'react-mentions';
import { Editor } from '@tinymce/tinymce-react';

import { IUser } from '@typings/db';

interface Props {
  onSubmitForm: (e: any) => void;
  chat?: string;
  onChangeChat: (e: any) => void;
  placeholder: string;
  data?: IUser[];
}

const EditorChatBox: FC<Props> = ({ onSubmitForm, chat, onChangeChat, placeholder, data }) => {
  return <Editor />;
};

export default EditorChatBox;

일단 에디터를 잘 불러오는지 한번 채널의 에디터를 한번 교체해보자.

// /pages/Chennel/index.tsx
// ...
import { Editor } from '@tinymce/tinymce-react';
// ...
const Channel = () => {
	// ...
  	return (
    	//...
        {/* <ChatBox
          onSubmitForm={onSubmitForm}
          chat={chat}
          onChangeChat={onChangeChat}
          placeholder={`Message #${channel}`}
          data={channelMembersData}
        /> */}
        <EditorChatBox
          onSubmitForm={onSubmitForm}
          chat={chat}
          onChangeChat={onChangeChat}
          placeholder={`Message #${channel}`}
          data={channelMembersData}
        />
      	//...
    )
  	// ...
}


에러가 나면서 에디터가 뜨지 않는다. 해당 에러에 대해 구글링 해보니 표준모드를 작성하지 않았다한다.
html에 DOCTYPE이 설정되어 있지 않기 때문이라고 스택오버플로우 형들이 알려줬다.
해당 오류에 대한 질문과 답변
그러면 이제 index.html 최상단으로 가서 doctype을 작성해주자.

<!-- 추가 -->
<!DOCTYPE html>
...

하고나서 다시 페이지를 열어보니 에디터를 잘 불러온다.

그럼 이제 apikey를 넣고 기능들을 붙여보자.


2-2. 에디터 플러그인 추가

api키를 넣으면 여러 플러그인을 등록하고 툴바에 등록 할 수 있다.
일단 없어지거나 이름이 바뀐 플러그인들도 있기때문에 좀 걸러내고 주로 많이 사용하는 플러그인들만 등록해보자.

<Editor 
	apiKey={'api key'}
	id='tinyMce_editor'
	init={{
    	height: 300,
        menubar: false,
        plugins: [
        	'lists',
            'link',
            'image',
            'charmap',
            'preview',
            'searchreplace',
            'fullscreen',
            'media',
            'table',
            'code',
            'help',
            'emoticons',
            'codesample',
            'quickbars',
        ]
        toolbar:
            'undo redo | blocks | ' +
            'bold italic forecolor | alignleft aligncenter ' +
            'alignright alignjustify | bullist numlist outdent indent | ' +
            'lists table link charmap searchreplace | ' +
            'image media codesample emoticons fullscreen preview | ' +
            'removeformat | help ',
	}}
/>

menubar옵션은 왠만하면 false로 사용하지 않는 편이 더 나은 것 같아서 꺼버렸다. 내가 등록하지 않은 플러그인들 기능까지 모두 있기 때문이다.

툴박스에 아이콘도 잘 출력되고 api를 연결해야하는 부분을 제외하고는 잘 작동 하고 있다.


3. 에디터 이벤트 등록

이제 에디터에 onsubmit이벤트와 onchange이벤트 그리고 이미지 업로드등의 기능을 살려보자.

3-1. 에디터 onChange 이벤트 등록

이제 텍스트에 작성할 값들을 커스텀훅스로 체인지 이벤트등록을 해보자.

// /components/EditorChatBox/index.tsx

import React, { FC, useCallback, useEffect, useRef } from 'react';
import autosize from 'autosize';
import gravatar from 'gravatar';
import { Mention, SuggestionDataItem } from 'react-mentions';
import { Editor } from '@tinymce/tinymce-react';

import { IUser } from '@typings/db';

interface Props {
  onSubmitForm: (e: any) => void;
  chat?: string;
  onChangeChat: (e: any) => void;
  placeholder: string;
  data?: IUser[];
}

const EditorChatBox: FC<Props> = ({ onSubmitForm, chat, onChangeChat, placeholder, data }) => {
  const onEditorChange = useCallback((value) => {
    onChangeChat(value);
  });
  return (
  	<Editor 
      apiKey={'api key'}
	  id='tinyMce_editor'
      initialValue={placeholder}
	  value={chat}
      onEditorChange={onEditorChange}
	  init={{
    	height: 300,
        menubar: false,
        plugins: [
          'lists',
          'link',
          'image',
          'charmap',
          'preview',
          'searchreplace',
          'fullscreen',
          'media',
          'table',
          'code',
          'help',
          'emoticons',
          'codesample',
          'quickbars',
        ]
        toolbar:
          'undo redo | blocks | ' +
          'bold italic forecolor | alignleft aligncenter ' +
          'alignright alignjustify | bullist numlist outdent indent | ' +
          'lists table link charmap searchreplace | ' +
          'image media codesample emoticons fullscreen preview | ' +
          'removeformat | help ',
	    }}
    />
  );
};

export default EditorChatBox;

다만 주의해야할 점은 기존의 useInput 훅스에서 [value, handler, setValue] 배열을 내보내서 인풋의 onchange 이벤트에 handler 함수를 작동 시켰으나, 여기에서는 setValue를 시켜주어야 한다. 에디터는 인풋엘리먼트가 아니기 때문에 event.target.value의 값을 얻을 수 없기 때문이다. 그래서 setValue를 onchange 함수를 대신해 props로 내려주고 에디터 컴포넌트 내부에서 useCallback으로 감싸서 setValue해주도록 하자.

// /pages/Chennel/index.tsx
// ...
import { Editor } from '@tinymce/tinymce-react';
// ...
const Channel = () => {
	// ...
  	return (
    	//...
        {/* <ChatBox
          onSubmitForm={onSubmitForm}
          chat={chat}
          onChangeChat={onChangeChat}
          placeholder={`Message #${channel}`}
          data={channelMembersData}
        /> */}
        <EditorChatBox
          onSubmitForm={onSubmitForm}
          chat={chat}
          onChangeChat={setChat}
          placeholder={`Message #${channel}`}
          data={channelMembersData}
        />
      	//...
    )
  	// ...
}

3-2. 폼에 onSubmit 이벤트 등록

그럼 이제 form으로 감싸고 버튼을 만들어 onsubmit까지 해서 api요청 하는 부분을 살려보자.

// /components/EditorChatBox/index.tsx

import React, { FC, useCallback, useEffect, useRef } from 'react';
import autosize from 'autosize';
import gravatar from 'gravatar';
import { Mention, SuggestionDataItem } from 'react-mentions';
import { Editor } from '@tinymce/tinymce-react';

import { IUser } from '@typings/db';

interface Props {
  onSubmitForm: (e: any) => void;
  chat?: string;
  onChangeChat: (e: any) => void;
  placeholder: string;
  data?: IUser[];
}

const EditorChatBox: FC<Props> = ({ onSubmitForm, chat, onChangeChat, placeholder, data }) => {
  const onEditorChange = useCallback((value) => {
    onChangeChat(value);
  });
  return (
    <form onSubmit={onSubmitForm}>
      <Editor 
        apiKey={'api key'}
        id='tinyMce_editor'
        initialValue={placeholder}
        value={chat}
        onEditorChange={onEditorChange}
        init={{
          height: 300,
          menubar: false,
          plugins: [
            'lists',
            'link',
            'image',
            'charmap',
            'preview',
            'searchreplace',
            'fullscreen',
            'media',
            'table',
            'code',
            'help',
            'emoticons',
            'codesample',
            'quickbars',
          ]
          toolbar:
            'undo redo | blocks | ' +
            'bold italic forecolor | alignleft aligncenter ' +
            'alignright alignjustify | bullist numlist outdent indent | ' +
            'lists table link charmap searchreplace | ' +
            'image media codesample emoticons fullscreen preview | ' +
            'removeformat | help ',
          }}
      />
	  <button type='submit'><i className="c-icon c-icon--paperplane-filled" aria-hidden="true" /></button>
	</form>
  );
};

export default EditorChatBox;


태그가 글자로 나오는것은 에디터작업이 끝난 후에 다뤄보자.


3-3. 에디터 이미지 업로드

에디터에 이미지 업로드는 비동기 요청으로 이미지를 먼저 업로드 한 이후에 url을 받아서 에디터 내부에 뿌려주게 된다. 그래서 글을 작성하지 않아도 에디터에 작성을 하게 되면 이미지 업로드가 된다.
제로초님께서 만드신 api에 이미지 업로드에서 워크스페이스와 채널있어야 하기 때문에 에디터 컴포넌트도 props로 워크스페이스와 채널명을 받고, tinymce의 이미지업로드 핸들러를 이용해 이미지를 업로드하고 url을 받아보자.

// /components/EditorChatBox/index.tsx

import React, { FC, useCallback, useEffect, useRef } from 'react';
import autosize from 'autosize';
import gravatar from 'gravatar';
import { Mention, SuggestionDataItem } from 'react-mentions';
import { Editor } from '@tinymce/tinymce-react';
import axios from 'axios';

import { IUser } from '@typings/db';

interface Props {
  onSubmitForm: (e: any) => void;
  chat?: string;
  onChangeChat: (e: any) => void;
  placeholder: string;
  data?: IUser[];
  channel: string;
  workspace: string;
}

const EditorChatBox: FC<Props> = ({ onSubmitForm, chat, onChangeChat, placeholder, data }) => {
  const onEditorChange = useCallback((value) => {
    onChangeChat(value);
  });
  return (
    <form onSubmit={onSubmitForm}>
      <Editor 
        apiKey={'api key'}
        id='tinyMce_editor'
        initialValue={placeholder}
        value={chat}
        onEditorChange={onEditorChange}
        init={{
          height: 300,
          menubar: false,
          plugins: [
            'lists',
            'link',
            'image',
            'charmap',
            'preview',
            'searchreplace',
            'fullscreen',
            'media',
            'table',
            'code',
            'help',
            'emoticons',
            'codesample',
            'quickbars',
          ]
          toolbar:
            'undo redo | blocks | ' +
            'bold italic forecolor | alignleft aligncenter ' +
            'alignright alignjustify | bullist numlist outdent indent | ' +
            'lists table link charmap searchreplace | ' +
            'image media codesample emoticons fullscreen preview | ' +
            'removeformat | help ',
          }},
          images_upload_url: process.env.NODE_ENV === 'production' ? 'api 도메인' : 'http://localhost:3095',
          images_upload_handler: (blobInfo) => 
            new Promise((resolve, reject) => {
              const formData = new FormData();
              formData.append('image', blobInfo.blob());
              axios
                .post(`/api/workspaces/${workspace}/channels/${channel}/images`, formData)
            	.then(res => {
              	  resolve(res);
              	})
                .catch(e => {
              	  reject(e);
              	});
            }),
      />
	  <button type='submit'><i className="c-icon c-icon--paperplane-filled" aria-hidden="true" /></button>
	</form>
  );
};

export default EditorChatBox;

그럼 이제 이미지를 업로드 해보자.
...에러가 발생했다.

일단 저 에러도 에러지만 한가지 또 문제가 있었다. 업로드 하고나면 그대로 글이 작성되는것이다. 업로드에는 성공했지만 에디터 내부에 뜨는 이미지는 blob상태로 내 로컬에 있는 이미지를 띄워주고 있는 상태고, 제로초님의 이미지업로드 api는 이미지를 업로드하고 그대로 글작성으로 insert되는 api였다. 에디터 이미지 업로드 api를 하나 더 만들어주어야 할것같다.

// /back/routes/api.js
// ...
router.post("/workspaces/:workspace/channels/:channel/images", isLoggedIn, upload.array("image"), async(req, res, next) => {...});
// 에디터 이미지 업로드 api 추가
router.post(
  "/workspaces/:workspace/channels/:channel/editorImages", 
  isLoggedIn, 
  upload.single('image'),
  (req, res, next) => {
    const locations = process.env.NODE_ENV === 'production' ? 'api 도메인' : 'http://localhost:3095'
    res.json({
      location:`${locations}/uploads/${req.file.filename}`
    });
  }
); 
// ...

tinyMCE에서 이미지 업로드하고 콜백해주는 json에서 키이름을 location이라고 지정해 주었다. 지난번엔 fileName이었다. 지금 tinyMce-react의 버전은 4.0.0에 tinyMce plugin버전은 6.x버전 인데 전에 사용했던 버전은 5.x버전이다. 그래서 문법들이 약간씩 바뀐게 있어 자료를 찾는데 조금 걸렸다. 6.x에 대한 버전 정리는 tinyMCE 플러그인 6 DOCS를 참고 했고, 5.x버전에 관련된 내용은 tinyMCE 플러그인 5 DOCS을 참고했다.

api업로드 url을 새로만든 api주소로 바꾸고 테스트해보자.

에디터에도 이미지가 로컬의 blob파일이 아니라 핸들러에서 받은 이미지 업로드 주소로 잘 들어가 있다.

그럼 이대로 다시 글 작성 버튼을 누르게 되면?..

허허 html 태그들이 그냥 스트링으로 출력 되고 있다. 에디터 처음 붙여서 포스트 테스트를 할때도 그대로 태그들이 스트링으로 출력 되었으니 다음 포스트에서 저런 에러 사항들을 정리해서 수정해보자.


profile
고급개발자되기

0개의 댓글