React-quill 라이브러리로 저장시 태그 없이 content만 보내기

2025. 10. 14. 16:45ㆍJS

게시판의 필수 기능인 게시글을 업로드할때 에디터가 필요하다고 생각했기에 react-quill-new 라이브러리를 선택하게 되었다.

 

그 이유는?

  • 커스터마이징하기에 좋다.
  • 기존 react-quill을 사용하려했으나 이벤트에 대해 리스너 관련 지원이 끊겨 더 최신 버전인 -new 라이브러리를 쓰게되었다.
  • npm에서 인기 있는 에디터 라이브러리이며 많이 쓰인다!

React-quill은 브라우저 환경에서는 잘 작동하지만, 서버 환경에서는 DOM API에 의존하므로 SSR시에 문제가 발생한다.

이때 Nextjs에서는 이런 경우에 사용하라고 dynamic import 를 지원한다.

그래서 클라이언트 사이드에서만 컴포넌트를 로드할 수 있게 해주는 것이다.

 

  const DynamicQuillEditor = dynamic(() => import("react-quill-new"), {
    ssr: false,
    loading: () => (
      <div style={{ minHeight: "350px" }}>에디터를 로딩 중...</div>
    ),
  });

 

* 핵심 옵션 *

ssr:false   => 클라이언트에서만 컴포넌트를 마운트

loading => 컴포넌트가 로드되는 동안 보여줄 fallback UI (선택적)

 

즉, 서버에서는 해당 컴포넌트를 건너뛰고, 브라우저에서만 React-quill이 로드되어 DOM API 관련 에러를 방지할 수 있다!

 

✔ 문제점 

: HTML태그와 내용이 같이 담기는 이슈 ,

  조회 시에도 HTML태그가 같이 보이는 이슈

 

이 react-quill-new 라이브러리를 사용해서 내용을 서버에 보내고 받아올때 html태그 없이 content 글 내용만 담겨야 하는데,

계속 이렇게 <p>태그가 같이 보이는 이슈가 발생 ...

 

수정요청할때는 html을 파싱해서 불필요한 공백/개행 문자를 제거해서 body값에 담기도록 removeHtmlTags 라는 함수를 만들어서

사용했다.

const removeHtmlTags = (html: string): string => {
            // 1. 임시 DOM 요소를 생성하여 HTML을 파싱.
            // DOMParser는 클라이언트(브라우저) 환경에서 사용 가능
            const doc = new DOMParser().parseFromString(html, "text/html");

            const text = doc.body.textContent || "";

            // 3. 불필요한 공백/개행 문자를 정리하여 반환.
            return text.trim();
};

  const plainContent = removeHtmlTags(formData.content);
  
     const body = {
            title: formData.title,
            content: plainContent,
     };

그랬더니

body 값에 태그 없이 잘 보내졌다.

 

하지만 수정이 아닌 등록할때는 왜 자꾸 html 태그가 붙어서 가는것일까? 

 

✔️해결해보자!

리액트에서는 기본적으로 HTML을 그대로 렌더링하지 않으므로, HTML 태그가 화면에 문자열 그대로 노출되는 근본적인 이유다.

리액트의 기본 동작은 XSS(Cross-site-scriptiong)공격 방지를 위해 모든 문자열을 이스케이프 한다.

입력한 문자열 리엑트가 렌더링하는 내용 화면에 보이는 내용
<h1>안녕</h1> &lt;h1&gt;안녕&lt;/h1&gt; <h1>안녕</h1>

 

이렇게 입력한 문자열을 리엑트는 그 안에 포함된 <, > 와 같은 특수문자를 &lt; , &gt; 형식으로 변환한다.

그래서 <p>됨?????</p>로 보이는 이유도 리액트가 보안상의 이유로 문자열을 이스케이프해서 일반 텍스트로 처리했기 때문이다!

 

첫번째 방법. 

그렇다면 , 이러한 리액트의 안전장치를 명시적으로 해제하고 HTML을 그대로 삽입하도록 해야하는데

이때, dangerouslySetInnerHTML 속성을 사용한다.

 

근데 이 속성에는 보안 취약성의 문제점이 존재한다. HTML을 그대로 삽입할 경우, 사용자가 악의적인 스크립트를 삽입할 가능성이 있는 XSS 공격에 취약해질 수 있다는 것이다.

 

두번째 방법. 

그래서 Quill에서 제공하는 Delta 라는 JSON 기반의 구조화된 데이터 형식을 사용해보려고 한다.

Delta는 문서의 내용과 서식을 JSON 형태로 표현하는 강력한 포맷이다. 

 

난 에디터를 form으로 관리하므로 formType의 content 타입을 any라 하고,

우선 defaultValues의 content를 {ops:[]}로 지정하였다. 

 

type FormType = {
  title: string;
  content: any;
  file: FileList | null;
};

  const form = useForm<FormType>({
    defaultValues: {
      title: "",
      content: { ops: [] }, // Delta 초기값
      file: null,
    },
  });

 

등록 요청시 서버에 전달할때 delta형식에서 순수 텍스트만 추출해서 보내면 된다!

 

만약 내가 content를 '소리질러'라고 입력하고 그냥 보낼경우

{"ops":[{"insert":"소리질러\n"}]}  이런식으로 전달된다.

 

🔎 Delta 형식

ops: operations의 약자로, 문서의 내용을 배열로 표현

insert: 삽입 할 텍스트

 

근데 난 여기서 '안녕하세요'만 출력해서 서버한테 보내야 한다.

const text = formData.content.ops
            .map((op: any) => op.insert)
            .join("")
            .trim();

 

1. 해당 ops 배열을 순회하여 각 operation 객체에서 입력된 내용을 담고 있는 insert 속성의 값만을 새로운 배열로 모은다.

2. join(): 추출된 텍스트 조각들의 배열을 긴 문자열로 연결(join)한다.

3. trim() : 앞뒤 공백 및 모든 서식 정보를 제거하여 정리.

 

그럼 이제 body에 담아서 보낼떄

body:{
 다른 속성들~
 content:text
}

 

 

이렇게 보내면 잘 된다!

실제 데이터에도 태그 없이 순수 텍스트만 담겨서 전달된다!