Quill은 리치 텍스트 에디터로, 일반 텍스트뿐만 아니라 볼드체, 이미지, 링크 등을 포함한 복잡한 컨텐츠를 편집할 수 있게 해주는 라이브러리이다. 일반 input이나 textarea와 달리 더 다양한 포맷팅 옵션을 제공한다.
const quillRef = useRef<typeof ReactQuill>();
이 코드는 React의 useRef 훅을 사용해 Quill 에디터 인스턴스에 대한 참조를 저장합니다. 이를 통해 에디터의 메소드에 직접 접근할 수 있다.
const quill = (quillRef.current as any).getEditor();
getEditor()는 Quill 인스턴스를 반환한다. 일반 input의 DOM 요소를 가져오는 것과 유사하지만, 여기서는 Quill이라는 복잡한 에디터 객체 전체를 가져온다. 이 객체에는 텍스트 조작, 서식 설정 등을 위한 여러 메소드가 포함되어 있다.
const quill = (quillRef.current as any).getEditor();
const text = quill.getText();
getText()는 에디터에 있는 모든 텍스트 내용을 HTML 태그 없이 순수 텍스트만 추출한다. 예를 들어 에디터에 "안녕하세요"라고 쓰여있다면 "안녕하세요"만 반환한다.
const range = quill.getSelection();
getSelection()은 현재 사용자가 에디터에서 선택한 부분(또는 커서 위치)에 대한 정보를 가져온다. 이 정보는 { index: 숫자, length: 숫자 } 형태의 객체로, index는 선택 시작 위치, length는 선택된 텍스트의 길이를 나타낸다.
quill.deleteText(5000, overLimit);
deleteText(start, length)는 지정된 시작 위치(start)부터 특정 길이(length)만큼의 텍스트를 삭제한다. 우리 코드에서는 5000자 위치부터 초과된 길이만큼 텍스트를 삭제한다.
const range = quill.getSelection();
quill.setSelection(Math.min(range.index, 5000), 0);
setSelection(index, length)는 에디터에서 특정 위치에 커서를 이동시키거나 텍스트를 선택한다. index는 선택 시작 위치, length는 선택할 텍스트 길이이다. 여기서 length가 0이면 해당 위치에 커서만 깜빡이게 된다. 우리 코드에서는 원래 커서 위치와 5000 중 작은 값으로 커서를 이동시킨다.
const updatedContent = quillRef.current ? (quillRef.current as any).getEditor().root.innerHTML : content;
root.innerHTML은 에디터의 현재 내용을 HTML 형식으로 가져온다. 에디터의 내용을 HTML 문자열로 얻기 위한 방법이다.
import React, { useRef, useState } from "react";
import dynamic from "next/dynamic";
import "react-quill/dist/quill.snow.css";
// 서버 사이드 렌더링 방지를 위한 동적 임포트
const ReactQuill = dynamic(
async () => {
const { default: RQ } = await import("react-quill");
const DynamicQuill = ({ forwardedRef, ...props }: any) => (
<RQ ref={forwardedRef} {...props} />
);
return DynamicQuill;
},
{ ssr: false }
);
function SimpleForm() {
// 폼 상태 관리
const [name, setName] = useState("");
const [content, setContent] = useState("");
// Quill 에디터 참조
const quillRef = useRef<any>();
// 일반 인풋 onChange 핸들러
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
};
// Quill 에디터 onChange 핸들러
const handleEditorChange = (html: string) => {
// html에는 에디터에 입력한 내용이 HTML 형식으로 들어있음
setContent(html);
// 에디터의 순수 텍스트만 확인하고 싶을 때
if (quillRef.current) {
const plainText = quillRef.current.getEditor().getText();
console.log("에디터 텍스트:", plainText);
}
};
// 폼 제출 핸들러
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// 폼 데이터 확인
console.log({
name,
content // HTML 형식의 텍스트
});
alert("폼이 제출되었습니다!");
};
return (
<form onSubmit={handleSubmit} className="p-4 max-w-lg mx-auto">
<div className="mb-4">
<label htmlFor="name" className="block mb-2 font-medium">
이름:
</label>
<input
type="text"
id="name"
value={name}
onChange={handleNameChange}
className="w-full px-3 py-2 border rounded-md"
required
/>
</div>
<div className="mb-4">
<label className="block mb-2 font-medium">
내용:
</label>
<div className="border rounded-md">
<ReactQuill
forwardedRef={quillRef}
value={content}
onChange={handleEditorChange}
placeholder="내용을 입력하세요"
modules={{
toolbar: [
['bold', 'italic'], // 기본 서식 도구
['link'] // 링크 삽입 도구
]
}}
/>
</div>
</div>
<button
type="submit"
className="px-4 py-2 bg-blue-500 text-white rounded-md"
>
제출하기
</button>
</form>
);
}
export default SimpleForm;
1. HTML로 저장하는 이유
볼드체, 이탤릭체, 링크, 글자 색상, 이미지 등 서식 정보를 유지하기 위해서다.
setContent(html) 방식으로 저장하면 아래와 같은 형태로 저장된다.
<p><strong>안녕하세요</strong> <em>반갑습니다</em></p>
2. 순수 텍스트로만 저장하면
리치 텍스트 에디터(Quill, TinyMCE, CKEditor 등)의 가장 큰 장점은 바로 이런 서식 정보를 유지할 수 있다는 점이다. 사용자가 글꼴, 크기, 색상, 정렬 등을 지정했을 때 이 정보를 HTML 태그와 스타일 속성으로 저장해서 나중에도 동일하게 표시할 수 있다.
따라서 블로그 포스트, 댓글, 메일 내용 등 서식이 중요한 텍스트는 대부분 HTML 형식으로 저장한다. 순수 텍스트는 검색, 요약, 글자 수 계산 등의 목적으로 필요할 때 HTML에서 추출해서 사용하는 것이 일반적이다.