안녕하세요. 제니입니다!
오늘은 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 설정
위에서 사용할 때 initialEditType="wysiwyg" 라고 되어 있는 부분이 바로
아래에 있는 언어설정입니다!
Markdown으로 하게 되면
이런 모양이 되므로, 저는 wysiwyg로 설정할게요!
1-2-2. EditType 숨기기
그리고 추가적으로
<Editor
initialValue="hello jenny"
previewStyle="vertical"
height="600px"
initialEditType="wysiwyg"
useCommandShortcut={false}
hideModeSwitch={true} // 추가
/>
저는 hideModeSwitch={true}를 추가해주었는데
이렇게 되면 editType을 사라지게 할 수 있습니다.
이렇게 아래 Markdown 선택하는 부분을 없앴습니다!
디자인에 따라 설정해주시면 될 것 같아요
2. 색상 설정 추가
yarn add @toast-ui/editor-plugin-color-syntax
기존 TOAST-UI 에는 색상 설정 기능이 없어서 위 플러그인을 설치해주었습니다!
2-1. 색상 플러그인 셋팅
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]} 를 추가해주시면 됩니다!
그렇게 되면
기존에 이러한 에디터 모습에서
색상 플러그인이 추가된 모습을 보실 수 있어요
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 함수
위에 hooks처럼 코드를 작성하면 type 이슈가 있어서
타입도 지정하구 넘어가려고 합니다.
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 설정
기존 코드에서 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을 확인해볼건데요
markdown과 html의 형태는 위와 같이 나오게 됩니다.
이렇게 보면 명확하게 차이를 보실 수 있어요!
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 설정했습니다.
오늘의 기록 끝!
'제니의 개발일지 > 도움이 되었던 것 정리' 카테고리의 다른 글
svg data url 형식 변환 base64 인코딩 적용법(cf. 이미지 경로 오류) (2) | 2024.05.24 |
---|---|
[코드 블럭] highlight 라이브러리 사용해서 code block 적용하기 (React, Typescript) (0) | 2024.04.04 |
[CSS] 글자 말 줄임 처리(feat. 반응형) (0) | 2024.03.26 |
[React] assets image 폴더 리팩토링 (0) | 2024.03.22 |
[React, Vite, TS] Vite alias 절대 경로 설정 (0) | 2024.03.22 |