React-quill

hyeseon han·2021년 10월 15일
6

웹 에디터 (React-quill)

React-quill 라이브러리

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

라이브러리를 사용하지 않고 내용을 담아오는 페이지를 구성할 때는 input태그와 textarea태그를 이용할 수 있다. 그런데 textarea태그에서 줄바꿈으로 내용을 입력하고 글을 등록했을 때 줄바꿈이 안되고 한 줄로 보여집니다. 폰트에 색깔을 추가하고 싶거나 텍스트에 스타일을 주고 싶을 수 있다. 이런 것들을 적용시킬 수 있게 도와주는 도구가 'React-Quill' 라이브러리이다.

//React-Quill 호출

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

React-Quill을 적용하면 이런 에러페이지가 뜬다. document는 브라우저

Next.js을 사용한다면 정상적인 에러이다. Next.js는 기본적으로 서버사이드 렌더링을 지원하는데 서버로부터 웹 페이지를 렌더링하는 시점에서는 window 또는 document object를 선언하기 전이기 때문에 document가 선언되지 않았다는 에러가 발생한다.

react 작동 방식
브라우저에서 주소 입력 → 프론트엔드 서버에서 html, css, js 받아서 브라우저로 가져와 그려준다.
(리액트는 프론트엔드 서버에서 먼저 그리지 않으므로 에러가 안난다.)

Next.js 작동 방식
프론트엔드 서버에서 먼저 한번 그리고, 이 그린걸 가지고 브라우저에도 그린다.
이 두 그림을 비교(dipping)
최종적인 dipping의 결과를 브라우저에 그린다.(hydration)

프론트엔드에서 먼저 그릴 때 문제가 발생한 것이다. 여기에는 브라우저(document)가 없다.

해결 방법:

  • React-Quill을 import하게 되는 시점을 document가 선언된 시점 이후 선언할 수 있게 해야한다.
  • Next.js의 dynamic(동적) import방식을 사용한다.(=프론트엔드 서버에서 그릴 땐 React-Quill을 import 하지말라는 뜻 )
import dynamic from "next/dynamic";
const ReactQuill = dynamic(() => import("react-quill"), { ssr: false });
  • dynamic: 해당 모듈을 호출하는 시점을 document에 대한 정보과 선언된 후의 시점으로 옮겨서 호출을 할 수 있게 도와준다.

React-Quill Custom

React-Quill에서 기본적으로 제공하는 기능들만 있다. 더 많은 기능들을 추가하려면 modules 속성을 활용할 수 있다.

React-Quill Docs https://quilljs.com/docs/modules/toolbar/

  • 웹 에디터가 추가되어있는 페이지에 modules상수를 생성한 후 웹 에디터 기능에 추가할 요소들을 새로 넣어준다.
const modules = {
    toolbar: {
        container: [
          [{ 'header': [1, 2, 3, 4, 5, 6, false] }],
          [{ 'font': [] }],
          [{ 'align': [] }],
          ['bold', 'italic', 'underline', 'strike', 'blockquote'],
          [{ 'list': 'ordered' }, { 'list': 'bullet' }, 'link'],
          [{ 'color': ['#000000', '#e60000', '#ff9900', '#ffff00', '#008a00', '#0066cc', '#9933ff', '#ffffff', '#facccc', '#ffebcc', '#ffffcc', '#cce8cc', '#cce0f5', '#ebd6ff', '#bbbbbb', '#f06666', '#ffc266', '#ffff66', '#66b966', '#66a3e0', '#c285ff', '#888888', '#a10000', '#b26b00', '#b2b200', '#006100', '#0047b2', '#6b24b2', '#444444', '#5c0000', '#663d00', '#666600', '#003700', '#002966', '#3d1466', 'custom-color'] }, { 'background': [] }],
          ['image', 'video'],
          ['clean']  
        ],
    }
}
  • toolbar: 이 container 안으로 배열 형태로 추가해주거나 제거해서 사용 할 기능을 커스텀 할 수 있다.
  • 추가한 modules 객체 데이터를 React-Quill을 호출하는 컴포넌트에 modules의 속석으로 설정해야한다.
export default function WebEditorPage() {
  const router = useRouter();
  const { handleSubmit, register, setValue, trigger } = useForm({
    mode: "onChange",
  });
  const [createBoard] = useMutation(CREATE_BOARD);
  // function onClickMyButton(){}

  async function onClickMyButton(data) {
    console.log(data);
    try {
      const result = await createBoard({
        variables: {
          createBoardInput: {
            writer: data.writer,
            password: data.password,
            title: data.title,
            contents: data.contents,
          },
        },
      });
      router.push(`/28-02-web-editor-detail/${result.data.createBoard._id}`);
    } catch (error) {
      console.log(error);
    }
  }

  function onChangeMyEditor(value) {
    // register로 등록하지 않고, 강제로 값을 넣어주는 기능
    setValue("contents", value === "<p><br></p>" ? "" : value);
    console.log(value);

    // register로 등록하지 않고, 강제로 값을 넣어주는 기능
    trigger("contents");
  }

  return (
    <form onSubmit={handleSubmit(onClickMyButton)}>
      작성자: <input type="text" {...register("writer")} />
      <br />
      비밀번호: <input type="password" {...register("password")} />
      <br />
      제목: <input type="text" {...register("title")} />
      <br />
      내용: <ReactQuill 
            onChange={onChangeMyEditor} 
            modules={modules}  // modules 속성으로 연결
           />
      <br />
      <button type="submit">등록하기</button>
      <br />
    </form>
  );
}
  • react-hook-form과 quill 강제로 연결시켜야한다.
  • setValue: 강제로 값을 집어넣는 도구
  • trigger: contents가 변경된걸 인식하여 바뀐 데이터가 저장된다.

입력된 웹 에디터의 내용을 화면에 출력

웹 에디터로 입력된 내용은 HTML 태그로 묶여서 입력이 되기 때문에 실제로는 HTML 태그들을 노출시키지 않은 상태에서 HTML 기능이 적용될 수 있도록 화면에 출력해야한다. 하지만, React에서는 기본적으로 HTML 보안 이슈로 인해 HTML 태그를 직접 삽입 할 수 없게 설정되어 있다.

dangerouslySetInnerHTML

HTML 태그를 사용하고자 한다면

<div dangerouslySetInnerHTML={{ __html :  HTML 태그 추가  }} />

dangerouslySetInnerHTML : div or span태그에서 제공되는 속성으로 위험성을 감수하고 HTML 태그를 추가할 때 사용하는 속성이다.

Dompurify

dangerouslySetInnerHTML 속성은 해킹에 취약한데 이를 방어하기 위해 Dompurify 라이브러리를 사용한다. 이 라이브러리의 sanitize 기능을 활용한다.

<Contents dangerouslySetInnerHTML={{
     __html: Dompurify.sanitize(data?.fetchBoard.contents),
    }}
></Contents>

() 안에 tag가 들어있으면 실행을 막아준다. 에러 발생
서버에서 렌더링 할 때 그려줄게 없어서 나는 에러인데, 브라우저가 없어서 그렇다. 따라서 서버에서 렌더링을 막아야한다.

방법1. 웹 브라우저가 있다면 그리고 없다면 그리지마.

  return (
    <>
      <div>작성자: {data?.fetchBoard.writer}</div>
      <div>제목: {data?.fetchBoard.title}</div>
      <div>
        내용:
        {typeof window !== "undefined" && <div
            dangerouslySetInnerHTML={{
              __html: Dompurify.sanitize(data?.fetchBoard.contents),
            }}></div>
        )}
      </div>
    </>
  );

방법 2. 프로세스 상태가 브라우저라면 그려라

  return (
    <>
      <div>작성자: {data?.fetchBoard.writer}</div>
      <div>제목: {data?.fetchBoard.title}</div>
      <div>
        내용:
        {process.browser && (
          <div
            dangerouslySetInnerHTML={{
              __html: Dompurify.sanitize(data?.fetchBoard.contents),
            }}></div>
        )}
      </div>
    </>
  );
import { useQuery, gql } from "@apollo/client";
import { useRouter } from "next/router";
import Dompurify from "dompurify";

const FETCH_BOARD = gql`
	...
`;

export default function WebEditorDetailPage() {
  const router = useRouter();

  const { data } = useQuery(FETCH_BOARD, {
    variables: {
      boardId: router.query.id,
    },
  });

  return (
    <>
      <div>작성자: {data?.fetchBoard.writer}</div>
      <div>제목: {data?.fetchBoard.title}</div>
      <div>
        내용:
        {process.browser && (
          <div
            dangerouslySetInnerHTML={{
              __html: Dompurify.sanitize(data?.fetchBoard.contents),
            }}
          ></div>
        )}
      </div>
    </>
  );
}

정리

React에서 사용할 때는 문제가 안되는 라이브러리인데, next.js에서는 문제가 된다. 왜냐하면 next.js는 서버 사이드 렌더링을 지원하는 react 프레임워크이기 때문이다.
이 라이브러리로 데이터를 받아서 보여줄 때 태그를 직접 읽히게 하지 않으면 string으로만 보여지므로 dangerous하게 넣어줬다. 공격성 태그들이 등록이 안되게 domqurify 라이브러리를 사용했다. domqurify는 브라우저에 있는 것들만 방어해주는데 브라우저가 없는 경우도 있으므로 조건부렌더링으로 브라우저일 경우만 보여지게 하였다.

0개의 댓글