장난감 연구소
[React] Next.js에서 Quill 에디터 사용하기 본문
이 글에는 Next.js에서 react-quill의 사용 없이 Quill 에디터를 사용하기 위한 코드를 제공합니다. 코드의 부족한 점과 문의 사항은 댓글 주시면 답변 및 수정하도록 하겠습니다.
샘플 프로젝트 다운로드
샘플 프로젝트는 create-next-app에서 일부분만 수정하여 위 사진과 같이 Quill 에디터를 사용해볼 수 있게 하였다.
해당 폴더로 이동 후 node module 설치 후 개발 서버 실행
$ npm install
$ npm run dev
아래 방식을 사용하게된 이유
Next.js를 통해 개발을 진행하면서 Quill 에디터를 사용하고자 했다. 그런데 Quill에서 바꾸고 싶은 부분이 있어 그 부분을 수정한 후 직접 빌드하여 사용해야 했다. 직접 빌드된 걸 react-quill에 적용하기 번거로워 보였다. 또, 어떤 블로그 글에서 react-quill을 사용하는 것이 직접 스크립트를 불러오는 것보다 번거롭다는 말이 있어 react-quill을 사용하지 않고 아래와 같은 방식으로 시도하였다.
이 글의 코드는 이 분의 블로그에서 소개된 내용에서 React Hook으로 변환 및 일부 코드를 추가한 것이다. 해당 블로그에서 React에서 Quill을 적용하는 방법을 올려주셔 잘 적용할 수 있었으나, 아쉽게 글을 내리신 걸로 보인다. 정말 큰 도움이 되어 감사하고 혹시 블로그 글에 링크를 걸은 것 때문에 삭제하신 거면 죄송하다고 전하고 싶다.
그래서 혹시 Next.js에서 Quill 에디터를 직접 로드하는 방식으로 사용하려는 분이 있다면 코드를 참고하라고 이 글을 올리게 되었다.
코드
/pages/index.js
export default function Home() {
const [body, setBody] = useState(''); // Quill 에디터의 innerHTML을 담는 state
const [mountBody, setMountBody] = useState(false); // 리렌더링 용도 state
/* 외부에서 body의 수정이 일어난 경우 body에 자동으로 적용되지 않습니다!
이 함수를 호출했을 때 컴포넌트 내의 useEffect가 실행되어 body의 수정 사항이 적용됩니다.*/
function rerenderBody() {
setMountBody(mb => !mb);
}
return (
<div className={styles.container}>
<Head>
<title>Quill Sample</title>
<link rel="icon" href="/favicon.ico" />
{/* 관련된 리소스 로드 (CSS는 _app.js에서 global CSS로 로드하는 편이 좋을 거 같다.)*/}
<link href="//cdn.jsdelivr.net/npm/katex@0.13.3/dist/katex.min.css" rel="stylesheet"/>
<script src="//cdn.jsdelivr.net/npm/katex@0.13.3/dist/katex.min.js"></script>
<script src="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.7.2/build/highlight.min.js"></script>
<script src="//cdn.quilljs.com/1.3.6/quill.min.js"></script>
<link rel="stylesheet" href="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.7.2/build/styles/default.min.css"/>
<link rel="stylesheet" href="//cdn.quilljs.com/1.3.6/quill.snow.css"/>
</Head>
<h1 className={styles.title}>
Quill Sample
</h1>
<div style={{ width: '80%', marginTop: '40px' }}>
<QuillEditor
body={body}
handleQuillChange={setBody}
mountBody={mountBody}
/>
</div>
<div style={{ width: '80%' }}>
<p>body state 미리보기</p>
{body}
</div>
<div>
<button onClick={() => { setBody((b) => (b + '<p>수정</p>')) }}>body 수정 발생</button>
<button onClick={rerenderBody}>body 수정 사항 적용</button>
</div>
</div>
)
}
관련 리소스 로드 (24번 줄 ~ 29번 줄)
<head>에서 quill.min.js 등의 스크립트 파일과 CSS 파일을 로드해야 한다. QuillEditor 컴포넌트 내에 이를 넣으면 QuillEditor 컴포넌트가 생길 때마다 똑같은 파일을 서버로부터 받아오니 script와 link는 각 페이지(/pages/index.js 등)마다 하나씩 넣어야 된다.
body State (8번 줄)
body는 Quill 에디터의 innerHTML을 담는 용도이다. Quill에서 text-change 이벤트가 일어날 때마다 setBody를 호출하여 업데이트한다.
rerenderBody (9번 줄~ 15번 줄)
아래 나오는 QuillEditor 컴포넌트의 useEffect 실행 조건에 body가 없어, 외부에서 body의 수정이 일어나도 body에 자동으로 적용되지 않는다. 외부에서 body 수정이 일어나면 rerenderBody 함수를 호출하면 body의 수정 사항이 적용된다.
/components/QuillEditor.js
export default function QuillEditor({ body, handleQuillChange, mountBody }) {
const quillElement = useRef();
const quillInstance = useRef();
const [isError, setIsError] = useState(false);
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
if (isLoaded) {
/* isLoaded가 true인 상태에서 rerenderBody를 통해 body 적용시 Quill 초기화 없이
innerHTML만 body로 바꿉니다. 이 조건이 없을 시 툴바가 중복되어 여러 개 나타나게
됩니다. */
const quill = quillInstance.current;
quill.root.innerHTML = body;
return;
}
if (quillElement.current && window.Quill) {
/* isLoaded가 false일 때는 Quill을 초기화합니다. */
/* Quill 옵션을 원하는 대로 수정하세요. */
const toolbarOptions = {
container: [
[{ 'size': ['small', false, 'large', 'huge'] }], // custom dropdown
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
[{ 'align': [] }],
['bold', 'italic', 'underline', 'strike'], // toggled buttons
[{ 'color': [] }, { 'background': [] }], // dropdown with defaults from theme
[{ 'header': 1 }, { 'header': 2 }], // custom button values
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'script': 'sub'}, { 'script': 'super' }], // superscript/subscript
[{ 'indent': '-1'}, { 'indent': '+1' }], // outdent/indent
[{ 'direction': 'rtl' }], // text direction
['clean'], // remove formatting button
['blockquote', 'link', 'code-block', 'formula', 'image', 'video'] // media
],
};
quillInstance.current = new window.Quill(quillElement.current, {
modules: {
history : {
delay: 2000,
maxStack: 500,
userOnly: true
},
syntax : true,
toolbar : toolbarOptions
},
placeholder: "본문 입력",
theme : 'snow'
});
const quill = quillInstance.current;
quill.root.setAttribute("spellcheck", "false");
// 초기 body state 적용
quill.root.innerHTML = body;
/* quill에서 text-change 이벤트 발생시에 setBody(innerHTML)을 통해 body를 업데이트합니다.
body가 업데이트되어도 useEffect 발생 조건 인자([isError, mountBody])에 body가 없으므로
QuillEditor 컴포넌트는 다시 렌더링되지 않습니다. 이는 입력 중 커서가 맨 앞으로 이동하는
문제를 방지합니다. 대신 외부에서 body가 수정되어도 rerenderBody가 호출되지 않으면 변경된
body가 적용되지 않습니다. */
quill.on("text-change", () => {
handleQuillChange(quill.root.innerHTML);
});
setIsLoaded(true);
} else {
/* quill.min.js가 로드되어 있지 않아 window.Quill이 undefined이면 isError가
계속 변경되면서 재시도합니다. */
setIsError((prevIsError) => (!prevIsError));
}
}, [isError, mountBody]);
return (
<div ref={quillElement}></div>
);
}
useEffect (10번 줄 ~ 76번 줄)
useEffect에서는 Quill의 초기화와 에디터의 innerHTML을 body로 적용하는 작업이 일어난다.
첫 컴포넌트 렌더링 시 quill.min.js가 로드되어 있지 않아 window.Quill이 undefined이면 isError가 계속 변경되면서 useEffect를 반복 실행한다.
window.Quill이 선언된 상태이면 Quill이 초기화되면서 quillElement 위치에 에디터가 표시되고, innerHTML로 body가 적용된다. 그리고 quill.on을 통해 text-change 이벤트가 발생할 때 handleQuillChange가 실행되도록 한다. 이때 body가 업데이트되어도 useEffect의 발생 조건 인자([isError, mountBody])에는 body가 없기 때문에 QuillEditor 컴포넌트는 다시 렌더링되지 않는다. 이는 매 입력마다 useEffect가 실행되면 입력 커서가 맨 앞으로 이동하는 문제가 있었기에 rerenderBody 함수가 호출될 때만 useEffect가 실행되도록 하였다.
툴바 옵션이나 Quill 생성자의 옵션 등은 Quill 공식 문서 페이지를 참고하여 원하는 대로 설정할 수 있다.
이미 Quill의 초기화가 완료된 상태(isLoaded가 true)에서는 useEffect 실행 시에 body의 변경 사항만 적용되도록 하였다.
샘플 프로젝트를 다운로드 받아 /components/QuillEditor.js 파일만 복사해 원하는 프로젝트 가져가고, /pages/index.js에서 어떻게 사용하는지만 확인하여 이식하면 바로 QuillEditor를 사용할 수 있을 것이다. 그 후 리소스 로드 URL, 툴바 옵션, Quill 옵션 등을 원하는 대로 수정바란다.
도움이 된 자료
'개발 > React' 카테고리의 다른 글
[경험] React에서 Quill 입력시 입력 오류 해결 (0) | 2021.01.30 |
---|