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> | <h1>안녕</h1> | <h1>안녕</h1> |
이렇게 입력한 문자열을 리엑트는 그 안에 포함된 <, > 와 같은 특수문자를 < , > 형식으로 변환한다.
그래서 <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
}
이렇게 보내면 잘 된다!


'JS' 카테고리의 다른 글
| mousedown -> pointerdown (0) | 2026.06.12 |
|---|---|
| [Hydration error 해결] mismatch 에러 (0) | 2026.01.22 |
| [Next.js] 로컬스토리지에 토큰이 안 담기는 이유 ? (0) | 2025.07.29 |
| [프론트] 바닐라JS로 가상돔 이해 및 구현하기 (1) | 2025.07.26 |
| [react 에러] Warning: A component is changing an uncontrolled input to be controlled. (인풋 에러) (5) | 2025.07.09 |