본문 바로가기
SPA/Next

[Next] Editor 사용하기 (1) - React Quill

by J4J 2022. 1. 6.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 React Quill Editor 사용하는 방법에 대해 적어보는 시간을 가져보려고 합니다.

 

 

 

들어가기에 앞서 다음 글도 참고해보시면 좋을 것 같습니다.

 

 

 

 

Next를 사용하면서 Editor를 구현해야 하는 상황이 생겨 사용할 수 있는 Editor를 찾고 테스트해본 결과 다음과 같이 3개의 패키지를 찾을 수 있었습니다.

 

  • react-quill
  • @toast-ui/react-editor
  • react-draft-wysiwyg

 

 

 

3개를 전부 깊게 사용해보지 않았지만 테스트 코드만 구현해봤을 때 사용성에 있어서 괜찮다고 생각한 순으로 작성해봤습니다.

 

그중 현재 가장 괜찮게 사용될 것 같다고 여겼던 react-quill에 대해 먼저 소개를 하고 그 이유에 대해서도 간단히 언급해보는 시간을 가져보겠습니다.

 

 

반응형

 

 

이미지 서버 설정

 

먼저 이미지 서버 설정 먼저 하겠습니다.

 

Editor를 사용할 때 이미지 업로드하는 기능을 거의 필수적으로 사용할 것이고 3개의 Editor 모두 이미지 업로드 기능을 제공해줍니다.

 

하지만 여기서 문제점은 업로드되는 이미지들은 자동으로 우리가 사용하는 서버에 적재되지 않습니다.

 

그렇기 때문에 이미지 업로드 기능을 커스텀하여 우리가 사용하는 서버에 이미지가 업로드가 되고 또한 업로드된 이미지를 화면에 보여줄 수 있도록 이미지를 관리하는 서버를 설정해줘야 합니다.

 

 

 

저는 해당 서버를 SpringBoot를 이용하여 구현했습니다.

 

코드는 다음과 같이 작성했고 필요하신 분만 참고해주시면 될 것 같습니다.

 

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Date;
import java.util.Random;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
public class ImageController {
	
	String UPLOAD_PATH = "F:\\myUpload"; // 업로드 할 위치
	
	// 이미지 불러오기
	@GetMapping("/getImage/{fileId}/{fileType}")
	public ResponseEntity<byte[]> getImageFile(@PathVariable String fileId, @PathVariable String fileType) {
		
		try {
			FileInputStream fis = new FileInputStream(UPLOAD_PATH + "\\" + fileId + "." + fileType);
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			
			byte buffer[] = new byte[1024];
			int length = 0;
			
			while((length = fis.read(buffer)) != -1) {
				baos.write(buffer, 0, length);
			}
			
			return new ResponseEntity<byte[]>(baos.toByteArray(), HttpStatus.OK);
			
		} catch(IOException e) {
			return new ResponseEntity<byte[]>(new byte[] {}, HttpStatus.CONFLICT);
		}
	}
	
	// 이미지 업로드
	@PostMapping("/uploadImage")
	public ResponseEntity<Object> uploadImage(MultipartFile multipartFiles[]) {
		try {
			MultipartFile file = multipartFiles[0];
            
			String fileId = (new Date().getTime()) + "" + (new Random().ints(1000, 9999).findAny().getAsInt()); // 현재 날짜와 랜덤 정수값으로 새로운 파일명 만들기
			String originName = file.getOriginalFilename(); // ex) 파일.jpg
			String fileExtension = originName.substring(originName.lastIndexOf(".") + 1); // ex) jpg
			originName = originName.substring(0, originName.lastIndexOf(".")); // ex) 파일
			long fileSize = file.getSize(); // 파일 사이즈
			
			File fileSave = new File(UPLOAD_PATH, fileId + "." + fileExtension); // ex) fileId.jpg
			if(!fileSave.exists()) { // 폴더가 없을 경우 폴더 만들기
				fileSave.mkdirs();
			}
            
			file.transferTo(fileSave); // fileSave의 형태로 파일 저장
			
			System.out.println("fileId= " + fileId);
			System.out.println("originName= " + originName);
			System.out.println("fileExtension= " + fileExtension);
			System.out.println("fileSize= " + fileSize);
			
			return new ResponseEntity<Object>("http://localhost:8080/getImage/" + fileId + "/" + fileExtension, HttpStatus.OK);
		} catch(IOException e) {
			return new ResponseEntity<Object>(null, HttpStatus.CONFLICT);
		}
	}
}

 

 

 

 

React Quill 사용 방법

 

[ 1. 패키지 설치 ]

 

$ npm install react-quill

 

 

 

[ 2. Editor 컴포넌트 생성 ]

 

/components 위치에 editor.tsx파일을 생성하여 다음과 같이 Editor를 만들어보겠습니다.

 

import * as React from 'react';
import styled from 'styled-components';
import axios from 'axios';
import { NextPage } from 'next';

import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
import { RangeStatic } from 'quill';

interface IEditor {
    htmlStr: string;
    setHtmlStr: React.Dispatch<React.SetStateAction<string>>;
}

const Editor: NextPage<IEditor> = ({ htmlStr, setHtmlStr}) => {

    const quillRef = React.useRef<ReactQuill>(null);

    // 이미지 업로드 핸들러, modules 설정보다 위에 있어야 정상 적용
    const imageHandler = () => {
        // file input 임의 생성
        const input = document.createElement('input');
        input.setAttribute('type', 'file');
        input.click();

        input.onchange = async() => {
            const file = input.files;
            const formData = new FormData();

            if(file) {
                formData.append("multipartFiles", file[0]);
            }

            // file 데이터 담아서 서버에 전달하여 이미지 업로드
            const res = await axios.post('http://localhost:8080/uploadImage', formData);

            if(quillRef.current) {
                // 현재 Editor 커서 위치에 서버로부터 전달받은 이미지 불러오는 url을 이용하여 이미지 태그 추가
                const index = (quillRef.current.getEditor().getSelection() as RangeStatic).index;

                const quillEditor = quillRef.current.getEditor();
                quillEditor.setSelection(index, 1);

                quillEditor.clipboard.dangerouslyPasteHTML(
                    index,
                    `<img src=${res.data} alt=${'alt text'} />`
                );
            }
        }
    }

    // useMemo를 사용하지 않고 handler를 등록할 경우 타이핑 할때마다 focus가 벗어남
    const modules = React.useMemo(() => ({
            toolbar: {
                // container에 등록되는 순서대로 tool 배치
                container: [
                    [{ 'font': [] }], // font 설정
                    [{ 'header': [1, 2, 3, 4, 5, 6, false] }], // header 설정
                    ['bold', 'italic', 'underline','strike', 'blockquote', 'code-block', 'formula'], // 굵기, 기울기, 밑줄 등 부가 tool 설정
                    [{'list': 'ordered'}, {'list': 'bullet'}, {'indent': '-1'}, {'indent': '+1'}], // 리스트, 인덴트 설정
                    ['link', 'image', 'video'], // 링크, 이미지, 비디오 업로드 설정
                    [{ 'align': [] }, { 'color': [] }, { 'background': [] }], // 정렬, 글씨 색깔, 글씨 배경색 설정
                    ['clean'], // toolbar 설정 초기화 설정
                ],

                // custom 핸들러 설정
                handlers: {
                    image: imageHandler, // 이미지 tool 사용에 대한 핸들러 설정
                }
            },  
        }
    ), [])

    // toolbar에 사용되는 tool format
    const formats = [
        'font',
        'header',
        'bold', 'italic', 'underline', 'strike', 'blockquote', 'code-block', 'formula',
        'list', 'bullet', 'indent',
        'link', 'image', 'video',
        'align', 'color', 'background',        
    ]

    return (
        <CustomReactQuill
            ref={quillRef}
            theme="snow" 
            modules={modules} 
            formats={formats} 
            value={htmlStr} 
            placeholder='내용을 입력하세요.'
            onChange={(content, delta, source, editor) => setHtmlStr(editor.getHTML())} />
    )
}

export default Editor;

// style
const CustomReactQuill = styled(ReactQuill)`
    height: 300px;
`

 

 

 

Editor 설정을 할 때 위에서 만들어 둔 이미지 서버를 활용하여 이미지가 업로드되고 또한 이미지를 불러올 수 있도록 핸들러를 추가 설정해줬습니다.

 

필요에 따라 이미지와 동일하게 다른 기능들도 추가 설정해줄 수 있습니다.

 

 

 

 

[ 3. Editor 사용하는 파일 작성 ]

 

Editor는 간단하게 index.tsx에서 불러와 사용해보도록 하겠습니다.

 

사용하기에 앞서 React로 사용할 때는 문제가 없지만 Next로 사용할 때 문제가 발생되는 것이 있습니다.

 

만들어진 Editor 컴포넌트를 로드할 때 Next에서 제공해주는 dynamic을 이용하여 ssr을 false로 설정해줘야 합니다.

 

설정해주지 않으면 Next에서는 에러와 인사를 하게 될겁니다.

 

import * as React from 'react';
import dynamic from 'next/dynamic';
import styled from 'styled-components';
import { NextPage } from 'next';

const Editor = dynamic(() => import('../components/editor'), { ssr: false }); // client 사이드에서만 동작되기 때문에 ssr false로 설정

const Index: NextPage = () => {
    // state
    const [htmlStr, setHtmlStr] = React.useState<string>('');

    // ref
    const viewContainerRef = React.useRef<HTMLDivElement>(null);

    // useEffect
    React.useEffect(() => {
        if(viewContainerRef.current) {
            viewContainerRef.current.innerHTML = '<h2>html 코드를 이용하여 만들어지는 View입니다.</h2>'
            viewContainerRef.current.innerHTML += htmlStr;
        }
    }, [htmlStr])

    return (
        <>
            <EditorContainer>
                <Editor htmlStr={htmlStr} setHtmlStr={setHtmlStr} />
            </EditorContainer>

            <Contents.Container>
                <Contents.HtmlContainer>
                    <h2>Editor를 통해 만들어진 html 코드입니다.</h2>
                    {htmlStr}
                </Contents.HtmlContainer>

                <Contents.ViewContainer ref={viewContainerRef} />
            </Contents.Container>
        </>
    );
};

export default Index;

// style
const EditorContainer = styled.div`
    width: 800px;
    height: 400px;

    margin: 0 auto;
`;

const Contents = {
    Container: styled.div`
        width: 1200px;
        
        margin: 0 auto;

        display: flex;
        gap: 40px;

        & > div {
            width: 600px;

            padding: 16px;

            box-sizing: border-box;
        }
    `,

    HtmlContainer: styled.div`
        border: 2px solid orange;
    `,

    ViewContainer: styled.div`
        border: 2px solid olive;

        // quill에서 가운데 정렬을 한 경우
        .ql-align-center {
            text-align: center;
        }

        // quill에서 코드 블럭을 사용한 경우
        .ql-syntax {
            background-color: #23241f;
            color: #f8f8f2;
            border-radius: 3px;
            padding: 5px;
            margin: 0 10px;
        }
    `,
}

 

 

 

 

테스트

 

코드를 위와 같이 작성한 뒤 실행하여 텍스트 작성 및 사진 업로드를 다음과 같이 해볼 수 있습니다.

 

실행 결과

 

 

 

상위에 있는 것은 Editor입니다.

 

좌측 하단에서는 Editor에 입력되는 내용들이 html 코드로 어떻게 보이는지를 확인할 수 있고 우측 하단에는 html코드를 이용하여 실제 화면에는 어떻게 보여지는지 확인할 수 있습니다.

 

 

 

 

특징

 

Editor를 찾아보기 전에 원하는 Editor 기능들은 다음과 같습니다.

 

  • 텍스트 정렬
  • 텍스트 색깔 지정
  • 이미지 업로드
  • 이미지 사이즈 조절
  • XSS 방지
  • 기타 등등...

 

 

 

이들 중 React Quill에서는 이미지 사이즈 조절하는 것을 제외하고는 대부분의 기능들을 제공해주고 있습니다.

 

또한 Editor 사용을 위한 코드 구현 과정도 복잡하지 않으며 원하는 tool들도 간편하게 추가 및 삭제도 할 수 있다고 느꼈습니다.

 

 

 

그리고 가장 마음에 들었던 부분은 만들어지는 html 코드입니다.

 

Editor에 내용을 작성할 때 텍스트 정렬과 같은 기능들을 사용하면 Editor에서 임의로 style을 추가해버리지 않고 class를 지정함으로 써 사용자가 원하는 스타일을 class에 맞게 설정하여 디자인할 수 있게 해 줍니다.

 

또한 이 외의 불필요한 class들도 임의로 사용되는 것이 없기 때문에 코드들을 깔끔하게 관리할 수 있을 것이라고 생각했습니다.

 

 

 

물론 아쉬웠던 부분도 있습니다.

 

원하는 Editor 기능 중 하나였던 이미지를 업로드한 뒤 사이즈를 조절하는 것이 없어서 아쉬웠고 또한 업로드된 이미지를 마우스로 드래그할 경우 동일한 이미지가 카피되지 않는 것도 아쉬웠었습니다.

 

물론 ctrl+c, v는 가능하지만요 ㅎㅎ...

 

이런 아쉬운 부분들이 있지만 Editor를 사용하는 것에 크게 불편함을 느끼지 않는 요소들이라고 생각을 하고 여러 장점들이 보이는 React Quill이 지금까지는 개인적으로는 가장 마음에 들었던 Editor였습니다.

 

 

 

 

 

 

 

 

 

이상으로 React Quill Editor 사용하는 방법에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글