장난감 연구소

[React] Next.js에서 Quill 에디터 사용하기 본문

개발/React

[React] Next.js에서 Quill 에디터 사용하기

changi1122 2021. 4. 25. 18:14

이 글에는 Next.js에서 react-quill의 사용 없이 Quill 에디터를 사용하기 위한 코드를 제공합니다. 코드의 부족한 점과 문의 사항은 댓글 주시면 답변 및 수정하도록 하겠습니다.

샘플 프로젝트 실행시 localhost:3000 접속시 웹페이지

샘플 프로젝트 다운로드

quill-sample.zip
0.10MB

샘플 프로젝트는 create-next-app에서 일부분만 수정하여 위 사진과 같이 Quill 에디터를 사용해볼 수 있게 하였다.

해당 폴더로 이동 후 node module 설치 후 개발 서버 실행

$ npm install
$ npm run dev

아래 방식을 사용하게된 이유

Next.js를 통해 개발을 진행하면서 Quill 에디터를 사용하고자 했다. 그런데 Quill에서 바꾸고 싶은 부분이 있어 그 부분을 수정한 후 직접 빌드하여 사용해야 했다. 직접 빌드된 걸 react-quill에 적용하기 번거로워 보였다. 또, 어떤 블로그 글에서 react-quill을 사용하는 것이 직접 스크립트를 불러오는 것보다 번거롭다는 말이 있어 react-quill을 사용하지 않고 아래와 같은 방식으로 시도하였다.

이 글의 코드는 이 분의 블로그에서 소개된 내용에서 React Hook으로 변환 및 일부 코드를 추가한 것이다. 해당 블로그에서 React에서 Quill을 적용하는 방법을 올려주셔 잘 적용할 수 있었으나, 아쉽게 글을 내리신 걸로 보인다. 정말 큰 도움이 되어 감사하고 혹시 블로그 글에 링크를 걸은 것 때문에 삭제하신 거면 죄송하다고 전하고 싶다.

그래서 혹시 Next.js에서 Quill 에디터를 직접 로드하는 방식으로 사용하려는 분이 있다면 코드를 참고하라고 이 글을 올리게 되었다.

코드

/pages/index.js




export default function Home() {

  const [body, setBody] = useState('');  // Quill 에디터의 innerHTML을 담는 state
  const [mountBody, setMountBody] = useState(false); // 리렌더링 용도 state

  /* 외부에서 body의 수정이 일어난 경우 body에 자동으로 적용되지 않습니다!
     이 함수를 호출했을 때 컴포넌트 내의 useEffect가 실행되어 body의 수정 사항이 적용됩니다.*/
  function rerenderBody() {
    setMountBody(mb => !mb);
  }

  return (
    <div className={styles.container}>
      <Head>
        <title>Quill Sample</title>
        <link rel="icon" href="/favicon.ico" />
        
        {/* 관련된 리소스 로드 (CSS는 _app.js에서 global CSS로 로드하는 편이 좋을 거 같다.)*/}
        <link href="//cdn.jsdelivr.net/npm/katex@0.13.3/dist/katex.min.css" rel="stylesheet"/>
        <script src="//cdn.jsdelivr.net/npm/katex@0.13.3/dist/katex.min.js"></script>
        <script src="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.7.2/build/highlight.min.js"></script>
        <script src="//cdn.quilljs.com/1.3.6/quill.min.js"></script>
        <link rel="stylesheet" href="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.7.2/build/styles/default.min.css"/>
        <link rel="stylesheet" href="//cdn.quilljs.com/1.3.6/quill.snow.css"/>
      </Head>

      <h1 className={styles.title}>
        Quill Sample
      </h1>

      <div style={{ width: '80%', marginTop: '40px' }}>
        <QuillEditor
          body={body}
          handleQuillChange={setBody}
          mountBody={mountBody}
        />
      </div>
      <div style={{ width: '80%' }}>
        <p>body state 미리보기</p>
        {body}
      </div>
      <div>
        <button onClick={() => { setBody((b) => (b + '<p>수정</p>')) }}>body 수정 발생</button>
        <button onClick={rerenderBody}>body 수정 사항 적용</button>
      </div>
    </div>
  )
}

관련 리소스 로드 (24번 줄 ~ 29번 줄)

<head>에서 quill.min.js 등의 스크립트 파일과 CSS 파일을 로드해야 한다. QuillEditor 컴포넌트 내에 이를 넣으면 QuillEditor 컴포넌트가 생길 때마다 똑같은 파일을 서버로부터 받아오니 script와 link는 각 페이지(/pages/index.js 등)마다 하나씩 넣어야 된다.

body State (8번 줄)

body는 Quill 에디터의 innerHTML을 담는 용도이다. Quill에서 text-change 이벤트가 일어날 때마다 setBody를 호출하여 업데이트한다.

rerenderBody (9번 줄~ 15번 줄)

아래 나오는 QuillEditor 컴포넌트의 useEffect 실행 조건에 body가 없어, 외부에서 body의 수정이 일어나도 body에 자동으로 적용되지 않는다. 외부에서 body 수정이 일어나면 rerenderBody 함수를 호출하면 body의 수정 사항이 적용된다.


/components/QuillEditor.js

export default function QuillEditor({ body, handleQuillChange, mountBody }) {
    const quillElement = useRef();
    const quillInstance = useRef();

    const [isError, setIsError] = useState(false);
    const [isLoaded, setIsLoaded] = useState(false);

    useEffect(() => {
        if (isLoaded) {
            /* isLoaded가 true인 상태에서 rerenderBody를 통해 body 적용시 Quill 초기화 없이
               innerHTML만 body로 바꿉니다. 이 조건이 없을 시 툴바가 중복되어 여러 개 나타나게
               됩니다. */
            const quill = quillInstance.current;
            quill.root.innerHTML = body;
            return;
        }
        if (quillElement.current && window.Quill) {
            /* isLoaded가 false일 때는 Quill을 초기화합니다. */

            /* Quill 옵션을 원하는 대로 수정하세요. */
            const toolbarOptions = {
                container: [
                    [{ 'size': ['small', false, 'large', 'huge'] }],    // custom dropdown
                    [{ 'header': [1, 2, 3, 4, 5, 6, false] }],
                    [{ 'align': [] }],
                    ['bold', 'italic', 'underline', 'strike'],          // toggled buttons
                    [{ 'color': [] }, { 'background': [] }],            // dropdown with defaults from theme
                    [{ 'header': 1 }, { 'header': 2 }],                 // custom button values
                    [{ 'list': 'ordered'}, { 'list': 'bullet' }],
                    [{ 'script': 'sub'}, { 'script': 'super' }],        // superscript/subscript
                    [{ 'indent': '-1'}, { 'indent': '+1' }],            // outdent/indent
                    [{ 'direction': 'rtl' }],                           // text direction
                    ['clean'],                                          // remove formatting button
                    ['blockquote', 'link', 'code-block', 'formula', 'image', 'video'] // media
                ],
            };

            quillInstance.current = new window.Quill(quillElement.current, {
                modules: {
                    history : {
                        delay: 2000,
                        maxStack: 500,
                        userOnly: true
                    },
                    syntax : true,
                    toolbar : toolbarOptions
                },
                placeholder: "본문 입력",
                theme : 'snow'
            });

            const quill = quillInstance.current;

            quill.root.setAttribute("spellcheck", "false");

            // 초기 body state 적용
            quill.root.innerHTML = body;

            /* quill에서 text-change 이벤트 발생시에 setBody(innerHTML)을 통해 body를 업데이트합니다.
               body가 업데이트되어도 useEffect 발생 조건 인자([isError, mountBody])에 body가 없으므로
               QuillEditor 컴포넌트는 다시 렌더링되지 않습니다. 이는 입력 중 커서가 맨 앞으로 이동하는
               문제를 방지합니다. 대신 외부에서 body가 수정되어도 rerenderBody가 호출되지 않으면 변경된
               body가 적용되지 않습니다. */
            quill.on("text-change", () => {
                handleQuillChange(quill.root.innerHTML);
            });

            setIsLoaded(true);
        } else {
            /* quill.min.js가 로드되어 있지 않아 window.Quill이 undefined이면 isError가
               계속 변경되면서 재시도합니다. */
            setIsError((prevIsError) => (!prevIsError));
        }
    }, [isError, mountBody]);


    return (
        <div ref={quillElement}></div>
    );
}

useEffect (10번 줄 ~ 76번 줄)

useEffect에서는 Quill의 초기화와 에디터의 innerHTML을 body로 적용하는 작업이 일어난다.

첫 컴포넌트 렌더링 시 quill.min.js가 로드되어 있지 않아 window.Quill이 undefined이면 isError가 계속 변경되면서 useEffect를 반복 실행한다.

window.Quill이 선언된 상태이면 Quill이 초기화되면서 quillElement 위치에 에디터가 표시되고, innerHTML로 body가 적용된다. 그리고 quill.on을 통해 text-change 이벤트가 발생할 때 handleQuillChange가 실행되도록 한다. 이때 body가 업데이트되어도 useEffect의 발생 조건 인자([isError, mountBody])에는 body가 없기 때문에 QuillEditor 컴포넌트는 다시 렌더링되지 않는다. 이는 매 입력마다 useEffect가 실행되면 입력 커서가 맨 앞으로 이동하는 문제가 있었기에 rerenderBody 함수가 호출될 때만 useEffect가 실행되도록 하였다.

툴바 옵션이나 Quill 생성자의 옵션 등은 Quill 공식 문서 페이지를 참고하여 원하는 대로 설정할 수 있다.

이미 Quill의 초기화가 완료된 상태(isLoaded가 true)에서는 useEffect 실행 시에 body의 변경 사항만 적용되도록 하였다.


샘플 프로젝트를 다운로드 받아 /components/QuillEditor.js 파일만 복사해 원하는 프로젝트 가져가고, /pages/index.js에서 어떻게 사용하는지만 확인하여 이식하면 바로 QuillEditor를 사용할 수 있을 것이다. 그 후 리소스 로드 URL, 툴바 옵션, Quill 옵션 등을 원하는 대로 수정바란다.

 

도움이 된 자료

HotHandCoding

'개발 > React' 카테고리의 다른 글

[경험] React에서 Quill 입력시 입력 오류 해결  (0) 2021.01.30