본문 바로가기

개발기/Save The Bunnies

4편: Quill 에디터를 내 마음대로 뜯어고치기

반응형

에디터에 폰트 색상 및 음영, 체크리스트를 추가한 모습

왜 Quill인가

가이드 콘텐츠는 리치 텍스트가 필요했습니다. 이미지, 글자색, 체크리스트까지 들어가야 했는데, Next.js 16 + React 19 조합에서 SSR과 충돌하는 에디터가 많았습니다. 결국 quill@2.0.3 코어를 직접 붙이고, dynamic(() => import(...), { ssr: false })로 클라이언트 전용 컴포넌트로 감쌌습니다.

새니타이징이 에디터 기능을 결정한다

가이드 본문은 백엔드에서 jsoup Safelist로 한 번 걸러집니다. 그런데 이 필터가 뭘 허용하느냐가 거꾸로 에디터에서 뭘 만들 수 있는지를 결정했습니다.

private static final Safelist QUILL_SAFELIST = Safelist.none()
        .addTags("p", "br", ...)
        .addAttributes("a", "href", "rel", "target")
        .addAttributes("img", "src", "alt", "width", "height")
        .addAttributes("span", "class", "style")
        .addAttributes("div", "class");

 

예를 들어 글자색/배경색 기능. Quill 기본 글자색 기능은 클래스 기반과 인라인 스타일 기반 두 가지 구현이 있는데, jsoup Safelist는 class 값을 검증하지 못하고 style 값 중 특정 CSS 속성만 걸러낼 수 있습니다. 그래서 클래스 기반이 아니라 인라인 style="color|background-color" 방식을 그대로 쓰기로 했습니다. 백엔드가 span style 중 이 두 속성만 통과시키도록 후처리를 넣는 식입니다.

체크리스트도 같은 이유로 선택이 갈렸습니다. <input type="checkbox">를 쓰면 저장/복원이 지저분해지길래, div/span 기반의 커스텀 Blot(ChecklistItemBlot)을 만들어 <div class="ql-checklist-item"> 구조로 저장하도록 했습니다. 대신 jsoup Safelist에 div.ql-checklist-item, span.ql-checklist-text, span.ql-checklist-box 세 클래스만 허용하는 후처리를 추가했습니다.

jsoup Safelist는 style/class 속성"값"을 검증하지 못한다. 그래서 태그+속성을 허용한 뒤 값 검증은 별도 후처리로 두 단계를 거친다.

에디터를 만드는 문제가 아니라 "새니타이저를 통과하는 구조를 먼저 정하고 에디터를 거기 맞추는" 문제였던 셈입니다.

이미지 업로드는 절대 URL로

이미지도 <img src="...">가 상대 경로면 jsoup이 걸러낼 수 있어서, 업로드 API가 항상 APP_BASE_URL 기반 절대 URL을 반환하도록 통일했습니다. /api/admin/images에 파일을 올리면 UUID 파일명으로 저장되고, 절대 URL이 그대로 에디터에 삽입됩니다.

이번 편 요약

  • Quill을 ssr:false로 클라이언트 전용 로드
  • 새니타이저(jsoup Safelist)가 에디터 기능 설계를 거꾸로 결정함 — 색상은 인라인 style, 체크리스트는 div/span 커스텀 Blot
  • 이미지 업로드는 절대 URL 고정 (상대 경로는 sanitize 과정에서 날아갈 수 있음)

다음 편에서는

증상 가이드 기능을 만들면서, "함께 보면 좋아요" 연관 기능을 다형적 구조로 갈아엎은 이야기를 다룹니다.

반응형