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)가 없다.
해결 방법:
import dynamic from "next/dynamic";
const ReactQuill = dynamic(() => import("react-quill"), { ssr: false });
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>
);
}
웹 에디터로 입력된 내용은 HTML 태그로 묶여서 입력이 되기 때문에 실제로는 HTML 태그들을 노출시키지 않은 상태에서 HTML 기능이 적용될 수 있도록 화면에 출력해야한다. 하지만, React에서는 기본적으로 HTML 보안 이슈로 인해 HTML 태그를 직접 삽입 할 수 없게 설정되어 있다.
HTML 태그를 사용하고자 한다면
<div dangerouslySetInnerHTML={{ __html : HTML 태그 추가 }} />
dangerouslySetInnerHTML
:div
orspan
태그에서 제공되는 속성으로 위험성을 감수하고 HTML 태그를 추가할 때 사용하는 속성이다.
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는 브라우저에 있는 것들만 방어해주는데 브라우저가 없는 경우도 있으므로 조건부렌더링으로 브라우저일 경우만 보여지게 하였다.