본문 바로가기

코드스테이츠 SEB FE 41기/Main-Project(MatP)

[react] 텍스트 에디터 react-quill 사용기 (feat. typescript)

반응형

우리 프로젝트에는 블로그 형식처럼 음식점에 대한 후기글을 남기는 기능이 있었기 때문에, text editor의 사용은 필수적이였다.

react에 적용되면서 typescript로 사용가능하고 레퍼런스가 많은 라이브러리를 찾아보려고 이틀 간 구글링을 정말 열심히 했던 것 같다! ㅠㅠ 타입스크립트로 작성된 플젝의 레퍼런스는 사막에서 바늘찾기... 난 아직 애기인걸.. 타입스크립트 신생아라서 레퍼런스 없이는 감도 안오는걸? 응애

결론적으로 내가 선택한 라이브러리는 react-quill이라는 라이브러리이다. react-quill 라이브러리는 타입스크립트가 적용될 뿐아니라 이미지 업로드 시 파일 업로더를 사용하는 방식으로 구현되어 있어, 유저가 게시글을 작성할 때 더욱 편리하게 이미지를 추가할 수 있겠다고 느껴 선택하게 되었다.

 

아래는 react - quill 라이브러리의 공식홈페이지 링크와

https://quilljs.com/playground/



 

Interactive Playground - Quill

 

quilljs.com

npm 설치 링크이다.

https://www.npmjs.com/package/react-quill

 

react-quill

The Quill rich-text editor as a React component.. Latest version: 2.0.0, last published: 5 months ago. Start using react-quill in your project by running `npm i react-quill`. There are 587 other projects in the npm registry using react-quill.

www.npmjs.com

 

이제 간단하게 적용 방법을 정리해보자.

1. 설치

npm install react-quill

2. index.html에 스타일시트 추가

<link rel="stylesheet" type="text/css" href="lib/css/normalize.css">

3. 에디터 컴포넌트 만들어주기

import React, { useRef, useState, useMemo } from "react";
import ReactQuill, { Quill } from "react-quill";
import "react-quill/dist/quill.snow.css";
import ImageResize from "@looop/quill-image-resize-module-react";

Quill.register("modules/imageResize", ImageResize);

type QuillEditorProps = {
  htmlContent: string;
  setHtmlContent: (htmlContent: string) => void;
};

const MatEditor = ({ htmlContent, setHtmlContent }: QuillEditorProps) => {
  const QuillRef = useRef<ReactQuill>();

  /**
   * quill에서 사용할 모듈을 설정하는 코드
   * useMemo를 사용하지 않으면, 키를 입력할 때마다, imageHandler 때문에 focus가 계속 풀림
   */
  const modules = useMemo(
    () => ({
      toolbar: {
        container: [
          ["bold", "italic", "underline", "strike", "blockquote"],
          [{ size: ["small", false, "large", "huge"] }, { color: [] }],
          [
            { list: "ordered" },
            { list: "bullet" },
            { indent: "-1" },
            { indent: "+1" },
            { align: [] },
          ],
          ["image"],
        ],
        handlers: {
          // image: imageHandler,
        },
      },
    }),
    []
  );

  return (
    <ReactQuill
      ref={(element) => {
        if (element !== null) {
          QuillRef.current = element;
        }
      }}
      value={htmlContent}
      onChange={setHtmlContent}
      modules={modules}
      theme="snow"
      placeholder="이미지를 한 개 이상 첨부하여 작성해주세요"
      style={{ width: "1200px", height: "500px" }}
    />
  );
};

export default MatEditor;

여기서 문제가 생겼다! ㅠㅠ 툴바에 있는 bold 기능 및 italic 기능이 제대로 동작하지 않았던 것 ㅠㅠ

결국 이 문제는 사용해주는 컴포넌트에서 css로 따로 설정을 해주었다 ㅜㅜ 왜 안되는지는 아직도 이해가 안되는 부분이다.

참고로 글을 적는 컨테이너 내 div의 heigth도 ReactQuill컴포넌트와 함께 측정되지 않고 아주 자기마음대로 따로 노는 현상이 발생해, 억지로 그 아이의 height도 같이 설정을 해주었다. 넌 또 왜그래??!! 

예를들어 ReactQuill 컴포넌트의 높이를 500px로 설정해주면  글을 적는 div 컨테이너의 높이도 갑자기 500px로 설정되어 내가 의도한 것보다 컴포넌트가 길어지는 효과가 있어 의아했다... 왜 포함이 안되고 따로 노는걸까?ㅋㅋㅋㅋㅋ

// 입력 div height
.ql-container.ql-snow {
    height: 450px;
  }

// bold
  .ql-editor p strong {
    font-weight: bold;
  }

//italic
  .ql-editor p em {
    font-style: italic;
  }

버그인지 뭔지는 모르겠지만 이처럼 따로 설정해주는 방식으로 해결했다.

프리프로젝트때 마크다운 에디터에서도 간간히 툴바 기능이 적용안되는 경우가 있었는데.. 왜 그런걸까? 

그냥 에디터도 힘든데 타입스크립트로 적용해보는 에디터라니... 뭐하나 쉽지가 않고만 ?!

어찌됐든 에디터를 띄우는 것까지는 성공했다.

이제 이미지 업로드 관련 처리를 해주자. 우리 프로젝트는 후기 지향 맛집 지도 컨셉을 가지고 있기에, 이미지의 존재가 무엇보다 중요했다.

하지만 react-quill 라이브러리에는 이미지 사이즈 조절 기능이 따로 없어, 관련 라이브러리를 하나 더 설치해서 추가 설정을 해주어야 한다.

이를 해결하는 데에 있어서도 무지 애를 먹었다. 구글링을 많이 해봤는데 거의 대부분의 이미지 리사이징 라이브러리가 적용되지 않았기 때문이다. 그러다가 한줄기 빛을 만나게 되었다.

npm i @looop/quill-image-resize-module-react

기도 메타는 역시 나를 실망시키지 않는다.

 

참고로 이 라이브러리를 쓰려면 declare를 해줘야 정상 작동한다(동적 임포팅때문이라고 하던데 이 부분은 아직 이해하지 못했다ㅠㅠ).

그래서 vscode에서 시키는 대로 type.d.ts 파일을 만들어 해당 문장을 추가해줬다.

declare module "@looop/quill-image-resize-module-react";

이처럼 declare를 해주면 아래와 같이 문제 없이 해당 라이브러리를 import를 해줄 수 있다.

import  ImageResize  from '@looop/quill-image-resize-module-react'
Quill.register('modules/ImageResize', ImageResize)

다시 에디터 컴포넌트로 돌아가보자.

하단의 코드를 modules 코드 내 toolbars 밑 부분에 추가해주면 어떤 이미지를 업로드하던 리사이징이 가능해진다.

imageResize : {
    	modules : ['Resize']
    }

엉엉 타스 너무 어려워 엄마

잘된다 잘돼...고마워 라이브러리야 ㅠㅠ

우리 팀은 이미지를 s3에 저장하여 url로 변환해주는 방식을 사용하였기 때문에 이미지 업로드 핸들링 함수가 필요했다.

/**
   *  이미지 핸들러(modules 설정보다 위에 있어야 정상 적용)
   */
  const imageHandler = () => {
    // 파일을 업로드 하기 위한 input 태그 생성
    const input = document.createElement("input");
    const formData = new FormData();
    let url = "";

    input.setAttribute("type", "file");
    input.setAttribute("accept", "image/jpg,impge/png,image/jpeg");
    input.click();

    // 파일이 input 태그에 담기면 실행 될 함수
    input.onchange = async () => {
      const file = input.files;

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

      try {
        // file 데이터 담아서 서버에 전달하여 이미지 업로드
        const res = await axios.post(
          "서버 배포 url/endpoint",
          formData,
          {
            headers: {
              "Content-Type": "multipart/form-data",
            },
          }
        );

        // 이미지 url
        url = res.data.data.path;

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

          if (index !== null && index !== undefined) {
            const quill = QuillRef.current?.getEditor();

            quill?.setSelection(index, 1);

            quill?.clipboard.dangerouslyPasteHTML(
              index,
              `<img src=${url} alt="이미지 태그가 삽입됩니다." />`
            );
          }
        }
      } catch (error) {
        const err = error as AxiosError;
        return { ...err.response, success: false };
      }
    };
  };

간단하게 작동 방식을 살펴보자.

  1. 에디터 내에 이미지가 삽입될 경우 파일을 업로드하기 위한 input 태그가 생성된다.
  2. 파일이 input 태그에 담기면 파일에 있는 데이터를 배포 서버에 요청을 보내 해당 이미지의 url을 리턴받는다.
  3. 처리가 완료되면 에디터 내 이미지가 업로드 된 커서 위치에 src 값이 해당 이미지의 url인 img 태그가 삽입된다.

참고로, 이미지 핸들러 함수는 modules 코드보다 위에 있어야 정상 작동된다.

  /**
   * quill에서 사용할 모듈을 설정하는 코드
   * useMemo를 사용하지 않으면, 키를 입력할 때마다, imageHandler 때문에 focus가 계속 풀림
   */
  const modules = useMemo(
    () => ({
      toolbar: {
        container: [
          ["bold", "italic", "underline", "strike", "blockquote"],
          [{ size: ["small", false, "large", "huge"] }, { color: [] }],
          [
            { list: "ordered" },
            { list: "bullet" },
            { indent: "-1" },
            { indent: "+1" },
            { align: [] },
          ],
          ["image"],
        ],
        handlers: {
          image: imageHandler,
        },
      },
      imageResize: {
        modules: ["Resize"],
      },
    }),
    []
  );

이제 modules의 handlers에 작성해준 이미지 Handler를 추가해주면 의도한 대로 이미지 핸들링을 할 수 있다.

 

참고 링크

https://isstar.tistory.com/9

 

React-Quill typeScript 적용

Quill? Quill은 최신 웹을 위해 만들어진 무료 오픈 소스 WYSIWYG 편집기로 모듈식 아키텍처와 표현식 API를 통해 필요에 따라 완벽하게 사용자 정의할 수 있다. 여러 편집기들이 있고 그 중 가장 대표

isstar.tistory.com

 

반응형