텍스트 에디터를 사용한 게시글 작성 구현

Donghwan Oh·2025년 5월 2일

게시판 기능을 구현하기 위해 텍스트 에디터를 사용하던 중 발생한 문제 상황과, 제가 해결한 방법을 공유하려고 합니다.

텍스트 에디터 라이브러리는 간편한 사용법으로 알려져 있는 react-quill을 사용하였습니다.

1. react와 react-quill 사이의 호환성 문제

2024년 12월 출시된 react 19버전에서는 react-quill 설치가 되지 않습니다.

대신 react-quill-new 라이브러리를 설치해 해결할 수 있었습니다.

2. 텍스트 에디터 스타일 변경

텍스트 에디터의 기본 스타일 파일은 node_modules의 라이브러리 폴더 안에 압축된 상태로 저장되어 있어 수정이 어려웠습니다.

저는 tailwindCSS의 @layer 기능을 사용하여 텍스트 에디터 요소가 가지고 있던 클래스에 base CSS를 추가해주었습니다.

@layer base {
  /* 텍스트 에디터 스타일 */
  .ql-toolbar {
    border-radius: 16px 16px 0 0;
    border: 3px solid #cad5e2 !important;
  }

  .ql-container {
    border-radius: 0 0 16px 16px;
    border: 3px solid #cad5e2 !important;
    border-top: 0 !important;
  }
}

라이브러리의 CSS가 적용된 이후에 tailwindCSS가 적용되므로 스타일이 겹치는 부분은 !important 키워드를 추가해 스타일을 덮어씌웠습니다.

3. react-hook-form에 에디터 컴포넌트 연결

react-hook-formregister, handleSubmit 등을 사용하여 인풋을 쉽게 관리할 수 있는 라이브러리입니다.

import PropTypes from 'prop-types';
import ReactQuill, { Quill } from 'react-quill-new';
import 'react-quill-new/dist/quill.snow.css';
import { useMemo } from 'react';

if (typeof window !== 'undefined' && window.Quill) {
  window.Quill = Quill;
}

// 에디터에서 사용할 기능을 제한
const formats = [
  'header',
  'bold',
  'italic',
  'underline',
  'strike',
  'blockquote',
  'list',
  'indent',
  'link',
  'color',
];

const Editor = ({ value, onChange, placeholder, height }) => {
  // 에디터에 표시할 기능
  const modules = useMemo(() => {
    return {
      toolbar: {
        container: [
          [{ header: [1, 2, false] }],
          ['bold', 'italic', 'underline', 'strike', 'blockquote'],
          [
            { list: 'bullet' },
            { list: 'ordered' },
            { indent: '-1' },
            { indent: '+1' },
          ],
          [{ color: [] }],
          ['link'],
          ['clean'],
        ],
      },
    };
  }, []);

  return (
    <div
      style={{ height: `${height + 43.05}px` }}
      className="w-full font-ownglyph"
    >
      <ReactQuill
        theme="snow"
        placeholder={placeholder}
        value={value}
        onChange={onChange}
        modules={modules}
        formats={formats}
        style={{ height }}
      />
    </div>
  );
};

Editor.propTypes = {
  value: PropTypes.string,
  onChange: PropTypes.func,
  height: PropTypes.number,
  placeholder: PropTypes.string,
};

export default Editor;

react-quill 라이브러리에서 제공하는 ReactQuill을 사용해 Editor 컴포넌트를 제작하였습니다.

ReactQuill의 props 중 value는 에디터에 입력된 값을, onChange는 에디터의 값이 수정되었을 때 실행되는 함수입니다.

Editor를 사용하는 컴포넌트에서 value를 관리하는 상태를 만들고, handleChange 함수에서 setValuevalue를 입력된 값으로 변경해주는 방식으로 에디터 값을 활용할 수 있습니다.

const PostWrite = () => {
  // ...
  const [value, setValue] = useState('');

  const handleChange = (event) => {
    setValue(event.target.value);
  };

  return (
    <div className="pb-8 lg:mx-0 md:-mx-8 sm:-mx-6">
    {/* ... */}
      <Editor
        placeholder={contentPlaceholder[boardType]}
        value={field.value}
        onChange={field.onChange}
        height={300}
        />
    {/* ... */}
    </div>
  );
};

export default PostWrite;

하지만 이렇게 되면 에디터 내의 값이 변경될 때마다 전체 컴포넌트의 리렌더가 발생하여 성능에 영향을 줄 수 있습니다.

react-hook-form의 register로 등록된 입력 컴포넌트를 추가적인 렌더링 없이도 관리할 수 있지만, ReactQuill이 다시 입력 컴포넌트를 감싸고 있기 때문에, register를 사용할 수 없었습니다.

이 때 react-hook-form의 Controller를 사용할 수 있습니다.

Controller를 외부 라이브러리의 입력 컴포넌트를 등록할 수 있는 컴포넌트로 다음과 같이 사용할 수 있습니다.

import { Controller, useForm } from 'react-hook-form';

const PostWrite = () => {
  // ...
  const { register, control, handleSubmit } = useForm();
  
  return (
    <div className="pb-8 lg:mx-0 md:-mx-8 sm:-mx-6">
        {/* react-hook-form의 Controller로 외부 인풋 라이브러리를 비제어 컴포넌트로 관리  */}
        <Controller
          name="content"
          control={control}
          render={({ field }) => (
            <Editor
              placeholder={contentPlaceholder[boardType]}
              value={field.value}
              onChange={field.onChange}
              height={300}
            />
          )}
        />
        {/* ... */}
    </div>
  );
};

export default PostWrite;

Controllercontrol로 react-hook-form에 등록되고, render를 사용해 UI를 표시합니다.

그리고 render 내부의 field를 통해 Editor의 폼 상태를 동기화합니다.

4. 에디터 내부 이미지 삽입 기능

react-quill는 텍스트 에디터 메뉴에서 이미지 삽입 기능을 제공합니다.

이미지를 추가하면 base64로 이미지가 인코딩되고, 이 경로를 태그 내부에 저장하여 사용됩니다.

base64로 인코딩된 이미지는 경로가 매우 깁니다. (3바이트 당 4개의 문자로 인코딩하므로 이미지 크기가 1MB인 경우 1024 x 1024 / 3 x 4 = 1,398,8000개의 문자열이 db에 저장됩니다.)

생성된 경로를 디코딩하는 방법이 있긴 하지만, 게시글을 불러올 때 img 태그를 감지해야는 문제가 추가적으로 있기 때문에 지금은 이미지 추가 입력 폼을 따로 구분하기로 했습니다.

또한 추가로 react-quill 에디터 내부 이미지 크기를 조절할 수 있는 라이브러리를 사용해보려 했지만, react-quill-new와 호환되는 것은 찾을 수 없었습니다.

이미지 입력 폼 추가하기 포스팅

5. 파싱된 html과 tailwindCSS 스타일의 중복

dompurify 라이브러리를 사용해 db에 저장된 html에서 위험한 태그를 제거하고, dangerouslySetInnerHTML로 이를 파싱하여 화면에 보여주었으나,

태그에 tailwindCSS의 리셋 스타일이 적용되어 에디터에서 작성하던 스타일이 모두 초기화되는 문제가 발생했습니다.

구글링을 통해 html을 표시하는 divql-editor라는 클래스를 추가해야한 다는 것을 알게 되었고,

<div
  className="ql-editor font-gowunbatang"
  dangerouslySetInnerHTML={{
    __html: DOMPurify.sanitize(postData.content),
  }}
  />

h1, h2, h3, quote, a 태그같이 추가 스타일이 필요한 경우 base에서 CSS를 작성해주었습니다.

.ql-editor h1 {
    font-size: 32px;
    font-weight: 600;
  }

  .ql-editor h2 {
    font-size: 24px;
    font-weight: 600;
  }

  .ql-editor blockquote {
    border-left: 4px solid #ccc;
    margin-bottom: 5px;
    margin-top: 5px;
    padding-left: 16px !important;
  }

  .ql-editor h3 {
    font-size: 16px;
  }

  .ql-editor a {
    color: blue;
    text-decoration: underline;
  }

profile
왜?에 대해 공부한 것을 기록합니다.

0개의 댓글