react-quill 과 Dompurify

Kimu·2021년 10월 13일
5
post-thumbnail

이렇게 멋진 텍스트 입력창을 제공하는 텍스트 에디터 라이브러리 react-quill을 소개합니다.
발음은 퀼 입니다.
지금 쓰는 벨로그의 텍스트 입력창처럼 html 꾸미기의 기본 기능을 제공하는 텍스트 입력창라이브러리 입니다.

이것을 사용하면 사용자가 텍스트를 입력시에 강조효과, 기울이기 등 편집을 할 수 있습니다.
그럼 되게 좋네요. 그냥 사용하면 되지, 뭐가 문제가 될까요?

이것을 사용해버리면, 발생할 수 있는 문제를 나열해보겠습니다.
1. 순수한 react에서는 import문제가 없지만 넥스트 등의 라이브러리 import 못함
2. 만약 useForm등의 라이브러러리로 입력창 변수를 관리할 경우, 변수의 value를 따로 받아야 하는 문제
3. 해킹의 위험(그리는 과정에서 해킹의 위험이 있는 innerhtml태그를 사용할 수 밖에 없음)

그럼, 1번 부터 차례대로 해결해볼까요?

먼저, 다운로드 받아봅시다. 터미널에서
yarn add react-quill
로 설치합니다.
그리고 npm의 설명서대로 사용하려는 페이지에서
import ReactQuill from 'react-quill'
이렇게 불러와봅시다. 불러와지나요?
아니요. 윈도우를 찾을 수 없다라는 오류가 뜹니다. 왜 그럴까요?

react-quill 이라는 라이브러리는 그냥 리액트에서는 import issue가 없지만 next.js를 사용하여 리액트를 실행할 경우 작동방식은 먼저 프론트엔드 서버에서 jsx를 가져와서 서버에서 한 번 그립니다(server side rendering: ssr).
그리고 클라이언트(사용자)의 컴퓨터에서 브라우저가 다운 받은 jsx를 한 번 더 브라우저에서 그려요.
그리고 두 개의 차이점을 비교합니다(diffing)
그 결과를 브라우저에게 알려주어 처리합니다(hydration)

브라우저에서 그리기 전에 프론트엔드 서버에서 ssr하는 과정에서는 당연히 브라우저가 아닌 환경이므로 윈도우가 없습니다. 따라서 오류를 발생합니다.

그래서 저렇게 평범하게 불러오면 안되고, 먼저 넥스트에서 다이내믹을 불러옵니다.
import dynamic from 'next/dynamic'
그리고

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

reactQuill을 변수에 담아주고, 다이내믹에서 불러오도록 합니다. ssr을 하지 않도록 false로 꺼주고요. 이렇게 불러와야 잘 불러와집니다.

이제 2번 문제를 해결해볼까요?

입력창의 값을 useState로 받는 구시대적인(?) 방법에서 벗어나, 현대인들의 리액트 라이브러리인 useForm을 사용하고 있습니다. 그런데, 또 다른 라이브러리인, 리액트 퀼도 사용해서 멋진 텍스트입력창을 구현하고 싶군요. 어떻게 방법이 없을까요?

있습니다. useForm에서 그런 기능을 제공합니다.
import { useForm } from "react-hook-form"
이미 이렇게 했겠죠?

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

useForm에서 굵게 쓰인 두 개의 변수명을 더 불러 옵니다. 저렇게 적어주면 불러온 거에요
💎 setValue는useForm을 사용하고 있지 않는 입력창의 입력값을 useForm안의 data로 강제로 값을 넣어주는 함수고, trigger는 useForm을 사용하고 있지 않은 입력창의 onChange를 리액트에게 알려주는 역할을 합니다.

react-quill에서 사용하는 onChange 함수안에서 값을 넣어줍니다.

function onChangeMyEditor(value) {
   // register로 등록하지 않고 강제로 값을 넣어주는 기능
   setValue("contents", value === "<p><br></p>" ? "" : value)
   console.log(value)
   // onChange 됐는지 react-hook-form에 알려주는 기능
   trigger("contents")
 }

그리고 jsx의 태그에서 함수를 바인딩해줍니다.

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} /><br />
        <button type="submit">등록하기</button>
        {/* <button type="button" onClick={onClickMyButton}>나의 버튼</button>
        <button type="reset">리셋하기</button> */}
    </form>
  )

이렇게 하면 내용에 입력한 값들이 useForm의 버튼에 걸릴 submit함수에서
data로 한 번에 같이 처리할 수 있게 됩니다.

const onClickMyButton = async (data) => {
  
 console.log("data:", 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)
  } 

참 쉽죠?ㅎ😉

자 이제 마지막,

해킹의 위험을 해결해야합니다.
아니 애초에 왜 해킹의 위험이 있는지부터 살펴봅시다.
위의 함수에서 data.contents를 콘솔로그로 찍어보면 값이 html태그와 함께 출력됩니다. 저 데이터를 그냥 가져와서 그려주면 우리는 html 태그까지 같이 출력이 됩니다. 이것은 우리가 원한 것이 아니죠? 우리가 원한 것은 사용자의 입력에 따라 태그의 효과가 적용된 html의 모습입니다. 그렇게 하기 위해서 우리는 그려주는 페이지에서 태그에 이렇게 적용해야합니다.

      <S.Contents dangerouslySetInnerHTML={{
        __html: props.data?.fetchUseditem.contents}}> </S.Contents>
      }

(여기서 S.Contents는 div를 감싸고 있는 emotion styled tag)
그려줄 태그 내에서 그려줄 내용을 dangerouslySetInnerHTML로 감싸서 내보내야 태그가 적용된 모습이 브라우저에 보여지게 됩니다. 그런데, innerHTML태그는 뭐다?
자바스크립트를 사용할 수 있는 입력창입니다. 해커가 여기에 태그를 써서 로컬에 저장된 정보를 열람하여 자기 서버로 보내는 코드를 짜놓는다면? 그것을 클릭하는 유저들의 정보가 털릴 위험이 있습니다. 물론 react-quill도 바보가 아니기때문에 "<"와 ">"를 괄호가 아니라 작다 크다를 의미하는 문자로 바꾸는 코드를 추가해두어서 그렇게 쉽게 털리지는 않습니다만, 아직도 위험은 존재합니다. (태그를 사용하지 않고도 해킹의 방법은 더 있으니까요)따라서 이런 위험을 막아줄 어떠한 방법이 필요합니다. 그것이 바로,
dompurify입니다. 빨리 설치를 합니다.

yarn add dompurify

라고 터미널에 입력합니다.
그리고, import Dompurify from "dompurify" 늘하듯이 이렇게 불러와서
적용하려고 하는데, 어라? 아까 봤던 오류가 또 납니다. 프론트엔드 서버에서 그려줄 때 브라우저가 아니라서 Dom이 없으니까 오류가 나는 것입니다. 아까 react quill은 import조차 안되었지만 지금의 경우는 import는 되는데 그려줄 때 생기는 문제이므로,

"process.browser &&" 이렇게 브라우저가 있을 때만 프로세스 할 수 있도록 중괄호 열고 적어줍니다.

 { process.browser && 
      <S.Contents dangerouslySetInnerHTML={{
        __html: Dompurify.sanitize(props.data?.fetchUseditem.contents)}}></S.Contents>
      } 

Dompurify.sanitize()안에 그려줄 변수명을 적어주면 여기서 적힌 js코드는 실행되지 않고 html만 그려줍니다. 그래서 위험이 없어지죠!

돔 퓨러파이. 참 고맙네요! 😘

그럼 오늘의 포스팅은 이만.

profile
지속가능한 개발자

2개의 댓글

comment-user-thumbnail
2021년 12월 12일

좋아요 :D

답글 달기
comment-user-thumbnail
2023년 10월 13일

좋은글 잘 보고가요!! 제 블로그에 해당글을 참고해서 글을 올려도 괜찮을까요?

답글 달기