본문 바로가기
제니의 개발일지/도움이 되었던 것 정리

[React] TOAST UI Editor 셋팅 + 커스텀 하는 법

by 제니운 2024. 3. 27.
728x90

 

 

 

 

안녕하세요. 제니입니다!

오늘은 Editor를 구현하게 되어서 해당 기록을 남겨보려고 합니다!

NHN 사의 TOAST-UI 라이브러리를 사용했고

프로젝트 환경은 React, TypeScript, yarn 입니다

 

 

1. TOAST-UI 설치

yarn add @toast-ui/react-editor

 

 

 

 

1-1. TOAST-UI 셋팅

import { Editor } from '@toast-ui/react-editor';
import '@toast-ui/editor/toastui-editor.css';

 

 

먼저 import를 해주세요.

 

 

완료 코드

 

 

 

코드의 전체 모습은 위와 같습니다.

import React from 'react';
import { Editor } from '@toast-ui/react-editor';
import '@toast-ui/editor/toastui-editor.css';

const EditorCommon = () => {
	return (
		<Editor
			initialValue="hello jenny"
			previewStyle="vertical"
			height="600px"
			initialEditType="wysiwyg"
			useCommandShortcut={false}
		/>
	);
};

export default EditorCommon;

 

 

 

Editor라고 컴포넌트명을 하고 싶었지만,,

import를 Editor를 하므로 EditorCommon이라고 이름 지었습니다.

 

 

 

1-2. Editor 사용

Editor에서 위에 나와있는 initialValue~use~ 이 부분들이 안 나오실경우 헷갈리실 수 있는데

복사붙여넣기 해서 넣어서 확인해주세요.

 

 

 

실제 사용

 

 

 

이렇게 사용하시면

 

 

 

현재 셋팅된 모습

 

 

 

이렇게 화면에 나오는걸 보실 수 있어요

 

 

 

1-2-1. EditType 설정

Editor 셋팅 상태

 

 

 

위에서 사용할 때 initialEditType="wysiwyg" 라고 되어 있는 부분이 바로

 

 

 

Editor 화면 아래

 

 

 

아래에 있는 언어설정입니다!

Markdown으로 하게 되면

 

 

 

Markdown일 때 Editor

 

 

 

이런 모양이 되므로, 저는 wysiwyg로 설정할게요!

 

 

 

1-2-2. EditType 숨기기

그리고 추가적으로

<Editor
    initialValue="hello jenny"
    previewStyle="vertical"
    height="600px"
    initialEditType="wysiwyg"
    useCommandShortcut={false}
    hideModeSwitch={true} // 추가
/>

 

 

 

저는 hideModeSwitch={true}를 추가해주었는데

이렇게 되면 editType을 사라지게 할 수 있습니다.

 

 

 

EditType 없앴을 때 Editor

 

 

 

이렇게 아래 Markdown 선택하는 부분을 없앴습니다!

디자인에 따라 설정해주시면 될 것 같아요

 

 

 

2. 색상 설정 추가

yarn add @toast-ui/editor-plugin-color-syntax

 

 

 

기존 TOAST-UI 에는 색상 설정 기능이 없어서 위 플러그인을 설치해주었습니다!

 

 

 

2-1. 색상 플러그인 셋팅

Editor plugin import

 

 

 

import를 추가해주는데요

 

 

 

import color from '@toast-ui/editor-plugin-color-syntax';
import 'tui-color-picker/dist/tui-color-picker.css';
import '@toast-ui/editor-plugin-color-syntax/dist/toastui-editor-plugin-color-syntax.css';

 

 

 

이렇게 color 플러그인과 css 두 가지를 import 해주게 됩니다.

 

 

 

const EditorCommon = () => {
	return (
		<Editor
			initialValue="hello jenny"
			previewStyle="vertical"
			height="300px"
			initialEditType="wysiwyg"
			useCommandShortcut={false}
			hideModeSwitch={true}
			plugins={[color]} // 추가
		/>
	);
};

export default EditorCommon;

 

 

 

위에서 작성한 Editor 코드에

plugins={[color]} 를 추가해주시면 됩니다!

그렇게 되면

 

 

 

기존 Editor

 

 

 

기존에 이러한 에디터 모습에서

 

 

 

색상 추가된 Editor

 

 

 

색상 플러그인이 추가된 모습을 보실 수 있어요

 

 

 

3. 이미지 처리하기

 

 

 

이미지를 업로드하게 되면 base64로 업로드가 되기 때문에, 서버 부하를 없애기 위해 링크만 보내주어야 해서

설정을 한 가지 더 해주어야 합니다!

 

 

 

const EditorCommon = () => {
	return (
		<Editor
			initialValue="hello jenny"
			previewStyle="vertical"
			height="300px"
			initialEditType="wysiwyg"
			useCommandShortcut={false}
			hideModeSwitch={true}
			plugins={[color]}
			hooks={{
				addImageBlobHook: async (blob, callback) => {
					const url = await ??
					callback(url, '');
				},
			}} // 추가
		/>
	);
};

export default EditorCommon;

 

 

 

 

이렇게 보시면 hooks에 addImageBlobHook을 추가해준 것을 보실 수 있는데,

저는 이 EditorCommon을  import해서 사용해줄 거라서 props로 넘겨주려고 합니다!

 

 

 

3-1. image handle 함수

blob, callback type 미 설정 시

 

 

 

위에 hooks처럼 코드를 작성하면 type 이슈가 있어서

타입도 지정하구 넘어가려고 합니다.

 

 

 

type 설정 함수 props

 

 

 

type EditorCommonProps = {
	handleImage: (blob: File, callback: typeof Function) => Promise<() => void>;
};

const EditorCommon = ({ handleImage }: EditorCommonProps) => {
	return (
		<Editor
			initialValue="hello jenny"
			previewStyle="vertical"
			height="300px"
			initialEditType="wysiwyg"
			useCommandShortcut={false}
			hideModeSwitch={true}
			plugins={[color]}
			hooks={{ addImageBlobHook: handleImage }}
		/>
	);
};

export default EditorCommon;

 

 

 

 

이렇게 handleImage 함를 넘기려고 합니다.

 

 

3-2 실제 사용

 

 

 

실제로 Editor를 사용하는 곳에서 함수까지 props 로 보내주게 되면

 

 

 

//
const handleImage = useCallback(async (file: File, callback: typeof Function) => () => {
    const url = await getImage(file)
    callback(url)
}, [])

return (
	//
	<EditorCommon handleImage={handleImage}/>
    //
)

 

 

 

 

이런식으로 보내주면 됩니다!

 

 

4. Editor 에 작성한 내용 출력하기

 

 

 

작성한 내용을 이제 받아야 합니다.

Editor에서 받을 수 있는 getMarkdown, getHTML 두 가지 방법이 있는데

그 방법을 사용하기 위해 먼저 ref 부터 설정할건데요

 

 

 

4-1. ref 설정

ref 설정

 

 

 

기존 코드에서 editorRef가 추가되었습니다.

 

 

 

type EditorCommonProps = {
	editorRef: React.RefObject<Editor> | null; // 추가
	handleImage: (blob: File, callback: typeof Function) => Promise<() => void>;
};

const EditorCommon = ({ editorRef, handleImage }: EditorCommonProps) => { // 추가
	return (
		<Editor
			ref={editorRef} // 추가
			initialValue="hello jenny"
			previewStyle="vertical"
			height="300px"
			initialEditType="wysiwyg"
			useCommandShortcut={false}
			hideModeSwitch={true}
			plugins={[color]}
			hooks={{ addImageBlobHook: handleImage }}
		/>
	);
};

export default EditorCommon;

 

 

 

4-2. 실제 사용하는 곳

import type { Editor } from '@toast-ui/react-editor';

 

 

 

먼저 import type을 해주시고

 

 

 

const editorRef = useRef<Editor>(null);

const onClickEnrollBtn = useCallback(() => {
    if (!editorRef.current) return;
    const markdown = editorRef.current.getInstance().getMarkdown();
    const html = editorRef.current.getInstance().getHTML();
    console.log('markdown', markdown);
    console.log('html', html);
}, []);

return (
<>
    <EditorCommon handleImage={handleImage} editorRef={editorRef} />
    <button onClick={onClickEnrollBtn}>버튼</button>
</>
)

 

 

 

실제로 사용하시는 곳에서 위와 같은 함수를 통해 console을 확인해볼건데요

 

 

 

console

 

 

 

markdown과 html의 형태는 위와 같이 나오게 됩니다.

 

 

 

console

 

 

 

이렇게 보면 명확하게 차이를 보실 수 있어요!

 

 

 

4-3. onChange  추가

 

 

 

버튼 클릭했을 때 말고, 바로바로  value 값을 얻고 싶으면 onChange를 추가해줄 수 있습니다.

 

 

 

type EditorCommonProps = {
	editorRef: React.RefObject<Editor> | null;
	handleImage: (blob: File, callback: typeof Function) => Promise<() => void>;
	placeHolder?: string;
	height?: string;
	onChangeEditor: () => void; // 추가
};

const EditorCommon = ({
	editorRef,
	handleImage,
	placeHolder = '내용이 없습니다.',
	height = '300px',
	onChangeEditor,
}: EditorCommonProps) => {
	return (
		<Editor
			ref={editorRef}
			initialValue=""
			previewStyle="vertical"
			height={height}
			initialEditType="wysiwyg"
			useCommandShortcut={false}
			hideModeSwitch={true}
			plugins={[color]}
			hooks={{ addImageBlobHook: handleImage }}
			placeholder={placeHolder}
			onChange={onChangeEditor} // 추가
		/>
	);
};

export default EditorCommon;

 

 

 

4-3-1. 실제 사용

 

 

const [htmlValue, setHtmlValue] = useState<string>('');
const [markdownValue, setMarkDownValue] = useState<string>('');

const onClickEnrollBtn = useCallback(() => {
    // todo: api 연결되면 htmlValue 값 보내기
}, []);

const onChangeEditor = useCallback(() => {
    if (!editorRef.current) return;
    const markdown = editorRef.current.getInstance().getMarkdown();
    const html = editorRef.current.getInstance().getHTML();
    console.log('markdown', markdown);
    console.log('html', html);
    setMarkDownValue(markdown);
    setHtmlValue(html);
}, []);

 

 

 

기존에 저는 onClickEnrollBtn 함수 내에 markdown, html을 추출 했는데

해당 내용을 onChange 함수 내로 옮겼고 그 값들을 state에 저장했는데요.

markdown, html을 둘 다 setState 한 이유는, 버튼 커스텀 때문입니다.

value 값이 없을 때 버튼을 비활성화 하고 싶은데 html은 태그랑 같이 나오게 되어서

markdown 값이 없을 경우 버튼 비활성화를 추가해주었습니다.

 

 

 

5. 커스텀 마무리

5-1. 초깃값, placeHolder

 

 

 

일단 저는 초깃값 설정해둔 것을 변경하고, 디자인에 맞춰 placeHolder를 설정해주었습니다.

초깃값의 경우, 아무것도 없는 데이터를 등록하는 것은 문제가 없는데

'수정하기' 버튼을 클릭할 경우, 기존 데이터가 남아있어야 학 때문에 props로 보내주는 것이 효율적으로 보였습니다.

없을 경우 공백으로 보내주도록 설정해두었어요

 

 

 

type EditorCommonProps = {
	editorRef: React.RefObject<Editor> | null;
	handleImage: (blob: File, callback: typeof Function) => Promise<() => void>;
    initialContent?: string;
};

const EditorCommon = ({ editorRef, handleImage, initialContent = "" }: EditorCommonProps) => {
	return (
		<Editor
			ref={editorRef}
			initialValue={initialContent} // 수정
			previewStyle="vertical"
			height="300px"
			initialEditType="wysiwyg"
			useCommandShortcut={false}
			hideModeSwitch={true}
			plugins={[color]}
			hooks={{ addImageBlobHook: handleImage }}
			placeholder="내용이 없습니다." // 추가
		/>
	);
};

export default EditorCommon;

 

 

 

Editor의 placeHolder는 카멜 케이스를 적용하지 않은 placeholder 입니다!

그런데 생각해보면 placeHolder는 사용하는 곳마다 다를 수 있어서

type으로 빼주려고 합니다.

 

 

 

type EditorCommonProps = {
	editorRef: React.RefObject<Editor> | null;
	handleImage: (blob: File, callback: typeof Function) => Promise<() => void>;
	placeHolder?: string;
};

const EditorCommon = ({ editorRef, handleImage, placeHolder = '내용이 없습니다.' }: EditorCommonProps) => {
	return (
		<Editor
			ref={editorRef}
			initialValue=""
			previewStyle="vertical"
			height="300px"
			initialEditType="wysiwyg"
			useCommandShortcut={false}
			hideModeSwitch={true}
			plugins={[color]}
			hooks={{ addImageBlobHook: handleImage }}
			placeholder={placeHolder}
		/>
	);
};

export default EditorCommon;

 

 

 

이렇게 최종적으로 처리해주고

placeHolder를 지정하지 않았을 때엔 '내용이 없습니다.'로 보이도록 설정했어요

 

 

 

5-2. height 변경

type EditorCommonProps = {
	editorRef: React.RefObject<Editor> | null;
	handleImage: (blob: File, callback: typeof Function) => Promise<() => void>;
	placeHolder?: string;
	height?: string;
};

const EditorCommon = ({
	editorRef,
	handleImage,
	placeHolder = '내용이 없습니다.',
	height = '300px',
}: EditorCommonProps) => {
	return (
		<Editor
			ref={editorRef}
			initialValue=""
			previewStyle="vertical"
			height={height}
			initialEditType="wysiwyg"
			useCommandShortcut={false}
			hideModeSwitch={true}
			plugins={[color]}
			hooks={{ addImageBlobHook: handleImage }}
			placeholder={placeHolder}
		/>
	);
};

export default EditorCommon;

 

 

 

 

height도 placeHolder처럼 사용하는 곳마다 다를 수 있어서 props로 보내주었고

설정 안했을 경우 300px 로 고정해두었는데, Editor에서 height는 string 을 보내야하기 때문에

헷갈리지 않도록 type은 string 설정했습니다.

 

 

 

 

오늘의 기록 끝! 

 

 

 

 

 

 

 

728x90