[Next.js] toast-ui editor 사용하기

JunSeok·2023년 1월 27일
1

Movie-inner 프로젝트

목록 보기
3/13
post-thumbnail
post-custom-banner

상황

토이 프로젝트에서 커뮤니티를 구현하기 위해 에디터를 사용하려 함

여러 에디터들이 있었으나 구현하기 간편한 toast-ui-editor 선택

구현

npm으로 다운받아준다.

npm i @toast-ui/react-editor

코드 구현 자세한 설명은 여기 공식 docs

공식 docs를 기반으로 구현했다.

특이사항

1. 제목을 따로 적는 부분이 없어서 내가 따로 input으로 추가해줬다.

2. toolbar item은 원하는 거 넣어주면 된다.

3. Editor 내의 여러 설정값은 원하는 값 넣어주면 된다. 구글링하면 쉽게 찾아볼 수 있다.

4. 이미지를 삽입하기 위한 hook이 Editor내에 존재한다.

바로 addImageBlobHook이다.매개변수에 blob과 callback 함수가 있다.
blob은 사용자가 삽입한 이미지가 base64 인코딩되어 나온다.
이를 fromData에 담아 서버로 보내준다.
서버에서는 AWS s3에 이미지를 저장 후 이미지를 불러올 수 있는 URL을 받아 리턴값으로 URL을 보내준다.
그리고 callback함수에 이미지 url을 입력해주면 글 화면에 이미지가 뜬다.

5. toast-ui-editor는 다 좋은데 이미지 사이즈 설정이 불가능했다.

아래와 같이 글을 작성했을 때

console.log('content', content)로 찍어보면 아래와 같이 나온다.

그래서 직접 이미지에 style 설정을 넣어주기로 했다. src가 나오기 전으로 position을 잡아서 앞 뒤로 자른 다음, 그 사이에 이미지 사이즈를 넣어주고 join해주었다.
이미지 사이즈는 max-width:20%으로 적당히 설정해주었다.

결과물은 다음과 같다.

완벽한 해결책은 아니지만 그래도 어찌저찌 해결한 모습이다.

6. 에디터 스타일 설정

height는 Editor 설정에 있는데, width는 없다. width 설정은 Editor를 div로 감싸준 다음 div에 width 값 설정을 해주었다.

7. ssr 설정 관련

toast-ui-editor는 ssr을 지원하지 않는다. 물론 그럴 필요도 없다.
react라면 그냥 넘어가면 되는 부분이다.
next.js라면 editor 사용시 dynamic import를 통해 ssr 설정을 꺼줄 필요가 있다.

작성한 Editor 컴포넌트를 ssr를 꺼준 옵션으로 import해주고 이를 리턴하는 컴포넌트를 실제 렌더링할 때 사용해주면 된다.

// write.tsx
// dynamic import를 통해 ssr 해제
import dynamic from 'next/dynamic'
const NoSsrWysiwyg = dynamic(() => import('./Editor'), { ssr: false })

const Write = () => {

    return <NoSsrWysiwyg />
}

export default Write

실제 구현 코드

// Editor.tsx
import { Editor } from '@toast-ui/react-editor'
import colorSyntax from '@toast-ui/editor-plugin-color-syntax'
import { useRef, useState } from 'react'
import { WriteContainer, WriteTitle, WriteBtn, WriteEditor } from './Write.style'
import { useRouter } from 'next/router'
import { apiInstance } from '../../../apis/setting'
import { toast } from 'react-toastify'
import { useSelector } from 'react-redux'
import { RootState } from '../../../store/store'

const WysiwygEditor = () => {
    const [image, setImage] = useState('')
    const router = useRouter()
    const [title, setTitle] = useState('') // 제목
    const editorRef = useRef(null)
    const toolbarItems = [['heading', 'bold', 'italic', 'strike'], ['hr'], ['ul', 'ol', 'task'], ['table', 'link'], ['image'], ['code'], ['scrollSync']]
    const userIdx = useSelector((state: RootState) => state.idx.idx)

    // 제목 설정
    const handleChange = (e) => {
        const { value } = e.target
        setTitle(value)
    }

    const onUploadImage = async (blob, callback) => {
        // blob은 base64 인코딩된 이미지 파일
        // formData에 담아 서버로 보내고, 서버에서는 s3에 이미지 저장후 s3에서 url을 받아 다시 프론트로 값 전송
        const formData = new FormData()
        formData.append('image', blob)
        try {
            const imageRes = await apiInstance.post('/image', formData, {
                headers: {
                    'Content-Type': 'multipart/form-data',
                },
            })
            const image_URL = imageRes.data.imageURL
            setImage(image_URL)
            // 글 화면에 이미지 띄우기
            callback(image_URL, 'image')
        } catch (e) {
            console.error(e.response)
        }
    }

    const showContent = async () => {
        const editorIns = editorRef.current.getInstance()
        // const HTML = editorIns.getMarkdown()
        const content = editorIns.getHTML()
        // console.log('html', HTML)
        console.log('title', title)
        console.log('content', content)
        console.log('image', image)
        const imageSize = 'style="max-width:20%"'
        const position = content.indexOf('src')

        const output = [content.slice(0, position), imageSize, content.slice(position)].join('')
        console.log('output', output)
        // 작성글 서버로 보내기
        try {
            const postContent = await apiInstance.post('/community/content', { userIdx: userIdx, title: title, content: output, file: image })
            router.replace('/community/feed')
            toast.success(`${postContent.data.idx} 번 글 작성 완료!`)
        } catch (e) {
            console.error(e.response)
        }
    }
    return (
        <WriteContainer>
            <WriteTitle type='text' placeholder='제목을 입력해주세요!' onChange={handleChange} />
            <WriteEditor>
                <Editor
                    ref={editorRef}
                    initialValue=''
                    placeholder='글을 작성해주세요!'
                    initialEditType='markdown'
                    previewStyle="tab"
                    height='60rem'
                    theme={'dark'}
                    toolbarItems={toolbarItems}
                    plugins={[colorSyntax]}
                    hooks={{ addImageBlobHook: onUploadImage }}
                />
            </WriteEditor>
            <WriteBtn>
                <button onClick={() => router.push('/community/feed')}>나가기</button>
                <button onClick={showContent}>저장</button>
            </WriteBtn>
        </WriteContainer>
    )
}

export default WysiwygEditor
profile
최선을 다한다는 것은 할 수 있는 한 가장 핵심을 향한다는 것
post-custom-banner

0개의 댓글