React Quill Text Editor 도입 후 이슈 해결하기

2025. 9. 27. 23:29·Programming/React

백오피스에서 기존 HTML 렌더링 방식에서 react-quill 텍스트 에디터 도입 과정에서 마주한 예상치 못한 문제들과 해결 과정을 공유한다.

 

텍스트 에디터 도입 배경

현재 백오피스에서는 서버에서 HTML 태그를 함께 내려주는 화면이 존재했다. 기존 방식은 조회 시에는 프론트에서 HTML을 렌더링하지만, 수정 시에는 HTML 태그가 그대로 노출된 상태로 편집해야 하는 불편함이 있었다.

해당 공통 컴포넌트는 다른 곳에서도 사용되고 있었는데, 조회 후 수정하는 방식이 아닌 새로 작성하는 템플릿에 대해서 텍스트 에디터가 필요하다는 니즈가 생겼다. 기본 기능들만 필요했고, 개발자가 사용하기 편리한 rich text editor 중 하나인 react-quill을 도입하게 되었다.

 

첫 번째 이슈: "들여쓰기가 안되었어요"

순간 마음이 철컹했다
왼쪽) 텍스트 에디터에서 작성 완료 후 서버 전송 이후 오른쪽) 다른 서버에서의 화면

텍스트 에디터 라이브러리를 도입한 후, CS팀을 통해 사용자 이슈가 들어왔다.

개발자 도구로 원인을 파악해보니, react-quill 라이브러리가 들여쓰기 Tab 키를 처리할 때 자체 CSS 클래스명을 생성한다는 것을 발견했다.

*<!-- Quill 에디터에서 생성된 HTML -->*
<ul>
  <li>첫 번째 항목</li>
  <li class="ql-indent-1">들여쓰기된 두 번째 항목</li>
  <li class="ql-indent-2">더 깊게 들여쓰기된 세 번째 항목</li>
</ul>

에디터에서는 제대로 들여쓰기가 보였지만, 실제 렌더링된 페이지에서는 모든 항목이 동일한 레벨로 표시되고 있었다. 핵심 문제는 ql-indent-1, ql-indent-2 같은 클래스에 대한 CSS 정의가 우리 서비스에 존재하지 않는다는 것이었다.

사용자가 의도한 포맷이 백오피스뿐만 아니라 이를 관리하는 다른 서버, 고객 서버에 모두 영향을 미친다는 점이었다.

우선 빨리 전달을 하자.

 

첫 번째 시도: &nbsp; 활용하기

가장 먼저 생각한 방법은 HTML 엔티티를 사용하는 것이었다

function addIndentWithSpaces(html) {
  return html.replace(/class="ql-indent-(\\d+)"/g, (match, level) => {
    const spaces = '&nbsp;'.repeat(level * 4);
    return `>${spaces}`;
  });
}

하지만 실제 테스트 결과 시각적으로 들여쓰기처럼 보이지 않았다. 단순한 공백 나열은 진짜 들여쓰기와는 거리가 멀었다.

 

두 번째 시도: Quill CSS 직접 참조

Quill 라이브러리의 기본 CSS에서 ql-indent-n 클래스 정의를 가져와서 우리 서비스에 추가하는 방법을 고려했다. 하지만 이 방법에는 몇 가지 우려점이 있었다.

  1. Quill에서는 기본 bullet을 숨기고 커스텀 아이콘을 사용하고 있어서, 그대로 가져오는 것이 적절한지 의문
  2. 라이브러리 의존성이 생겨버리는 것에 대한 부담

결국 이 방법은 최후의 수단으로 보류했다.

 

최종 선택: HTML 전처리를 통한 인라인 스타일 적용

온라인 HTML 테스트 환경에서 여러 CSS 속성을 실험해본 결과를 비교했다.

<li style="padding-left: 24px;">padding 사용</li>          *<!-- 기존 스타일과 충돌 -->*
<li style="text-indent: 24px;">text-indent 사용</li>       *<!-- 첫 줄만 들여쓰기 -->*
<li style="margin-left: 24px;">margin-left 사용</li>       *<!-- 가장 자연스러운 결과 -->*

 

margin-left를 선택한 이유는 단순했다. 전체 리스트 아이템을 일관되게 들여쓰기할 수 있고, 기존 스타일과 충돌하지 않으며, Quill의 자체 CSS에 의존하지 않는 독립적인 해결책이었기 때문이다.

 

HTML 파싱의 까다로운 점들

정규식으로 HTML을 파싱하는 것은 생각보다 복잡했다. 다양한 엣지 케이스를 고려해야 했다.

const testCases = [
  '<li class="ql-indent-1">기본 케이스</li>',
  '<li class="other-class ql-indent-2">다른 클래스와 혼재</li>',
  '<li class="ql-indent-1" style="color: red;">기존 스타일 존재</li>',
  '<li style="font-weight: bold;" class="ql-indent-2">스타일이 앞에 있는 경우</li>',
  '<li class="ql-indent-3" style="color: blue; margin-left: 10px;">기존 margin-left 존재</li>'
];

 

각각의 케이스에 대해 기존 속성을 손상시키지 않으면서도 안전하게 스타일을 추가하는 로직을 구현해야 했다.

export function applyQuillIndentInline(html: string): string {
  if (!html) return html;
  
  return html.replace(/<([a-zA-Z0-9]+)([^>]*)>/g, (full, tagName, attrs) => {
    const classMatch = attrs.match(/\\bclass\\s*=\\s*"[^"]*\\bql-indent-(\\d+)\\b[^"]*"/);
    if (!classMatch) return full;
    
    const indentLevel = Number(classMatch[1]);
    if (!Number.isFinite(indentLevel)) return full;
    
    const marginValue = 24 * indentLevel;

    *// 기존 style 속성이 있는 경우의 복잡한 처리*
    const existingStyleMatch = attrs.match(/\\bstyle\\s*=\\s*"([^"]*)"/i);
    if (existingStyleMatch) {
      const currentStyle = existingStyleMatch[1];
      *// 기존 margin-left가 있다면 제거 후 새 값 적용*
      const cleanedStyle = currentStyle.replace(/margin-left\\s*:\\s*[^;]+;?\\s*/i, '');
      const needsSemicolon = 
        cleanedStyle.trim().length > 0 && !/;\\s*$/.test(cleanedStyle);
      const updatedStyle = 
        `${cleanedStyle}${needsSemicolon ? ';' : ''}margin-left: ${marginValue}px;`;
      
      return full.replace(existingStyleMatch[0], `style="${updatedStyle}"`);
    }

    const enhancedAttrs = `${attrs}${attrs.trim().length ? ' ' : ''}style="margin-left: ${marginValue}px;"`;
    return `<${tagName}${enhancedAttrs}>`;
  });
}

 

두 번째 이슈: "왜 자꾸 빈 줄이 생기지?"

첫 번째 문제를 해결한 후 자체적으로 더 꼼꼼히 테스트를 해보니, 새로운 문제를 발견했다. 리스트를 작성하고 저장했다가 다시 편집하려고 하면 위에 빈 줄이 계속 생기는 현상이었다.

*<!-- 1차 저장 후 -->*
<p><strong>[변경 날짜]</strong></p>
<ul>
  <li>첫 번째 항목</li>
  <li>두 번째 항목</li>
</ul>

*<!-- 다시 편집 모드 → 저장 후 -->*
<p><strong>[변경 날짜]</strong></p>
<p><br></p>  *<!-- 이게 계속 추가됨 -->*
<ul>
  <li>첫 번째 항목</li>
  <li>두 번째 항목</li>
</ul>

*<!-- 또 다시 편집 → 저장하면 -->*
<p><strong>[변경 날짜]</strong></p>
<p><br></p>
<p><br></p>  *<!-- 계속 누적됨 -->*
<ul>
  <li>첫 번째 항목</li>
  <li>두 번째 항목</li>
</ul>

 

이 문제의 근본 원인은 react-quill의 HTML 처리 방식에 있었다.

  1. 에디터에서의 리스트 생성: react-quill 에디터 내에서 불릿을 작성할 때 <li> 태그와 함께 상단에 <ul> 태그가 자동으로 생성된다.
  2. 서버 저장 및 조회: 작성된 HTML이 서버에 전송되고, 다시 편집 모드로 진입할 때 서버에서 응답받은 HTML을 텍스트 에디터에 붙여넣는다.
  3. 자동 태그 삽입: 이때 Quill이 기존 HTML을 해석하면서 <ul> 상단에 <p><br></p> 태그를 자동으로 추가하게 된다

편집 → 저장 → 조회 → 편집.. 사이클이 반복될 때마다 불필요한 줄바꿈이 누적되는 구조적 문제였다.

 

두 번째 문제를 해결하기 위해 GitHub에서 관련 이슈를 찾아보기 시작했다.

처음에는 우리만의 특수한 문제일 거라 생각했는데, 검색 결과는 예상 밖이었다.

Quill #2905: "Quill inserts a useless line-break before the <ul> element"

이슈를 읽어보니 정말 많은 개발자들이 같은 문제로 고생하고 있었다. 특히 인상적이었던 댓글들,

  • "Every time we save and reload, another <p><br></p> gets added"
  • "This is breaking our production editor"
  • "Been struggling with this for months"

한 개발자가 제시한 간단하지만 효과적인 해결책을 발견했다.

const modules = {
  clipboard: {
    matchVisual: false,
  },
};

이 설정이 효과적인 이유는 Quill이 클립보드 콘텐츠를 처리할 때 시각적 형태를 유지하려다가 발생하는 부작용을 방지하기 때문이었다.

나의 고민과 해결 PR에 칭찬받은거 자랑하기

 

더 큰 문제의 발견: 라이브러리 생태계 이슈

하지만 이 과정에서 더 근본적인 문제를 발견하게 되었다.

  • react-quill의 마지막 업데이트가 3년 전
  • Quill 메인 라이브러리에는 이미 수정된 버그들이 React 래퍼에는 아직 반영되지 않음
  • 커뮤니티에서 제시하는 해결책들이 모두 "임시방편" 수준

이때 중요한 깨달음을 얻었다. 라이브러리 선택에서 단순히 기능과 사용 편의성만 보는 것이 아니라, 유지보수 상태와 생태계 건강도를 함께 평가해야 한다는 것이었다.

 

이번 경험을 통해 완벽해 보이는 라이브러리도 실제 운영 환경에서는 예상치 못한 문제를 일으킬 수 있다는 것을 배웠다. 중요한 것은 문제가 발생했을 때 체계적으로 접근하고, 커뮤니티의 지혜를 활용하며, 사용자 경험을 최우선으로 생각하는 것이다.

앞으로는 react-quilljs로의 마이그레이션을 계획하고 있다.

'Programming > React' 카테고리의 다른 글

useOverlay close 시 컴포넌트가 unmount되도록 처리하기  (0) 2025.09.14
SDUI (Server Driven User Interface)  (0) 2025.09.13
React Hook Form 렌더링 최적화  (0) 2025.06.22
React 프로젝트 의존성 버전 업그레이드 하기 - axios (0.18.0 → 1.7.7)  (1) 2024.11.21
Kakao 소셜 로그인: Local 환경에서는 되고, S3 + Cloudfront 배포 후는 안되는 문제 해결  (1) 2024.09.23
'Programming/React' 카테고리의 다른 글
  • useOverlay close 시 컴포넌트가 unmount되도록 처리하기
  • SDUI (Server Driven User Interface)
  • React Hook Form 렌더링 최적화
  • React 프로젝트 의존성 버전 업그레이드 하기 - axios (0.18.0 → 1.7.7)
gitit
gitit
짬내서 쓰는 프론트엔드와 기술 메모 공간
  • gitit
    깃잇-gitit
    gitit
  • 전체
    오늘
    어제
    • 분류 전체보기
      • Coding Test
        • Programmers
        • BackJoon
      • Development Tools
        • Firebase
        • Git
        • Monorepo
      • Programming
        • JavaScript
        • React
        • React-Native
      • etc.
        • GeekNews
        • Blockchain
      • Technical Interview
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    리액트
    styled-components
    기술 질문
    자바스크립트
    알고리즘
    node.js
    백준
    AWS
    기본문법
    modal
    javascript
    frontend
    프론트엔드
    긱뉴스
    React
    kakao
    BFS
    js
    Firebase
    파이썬
    코딩
    파이어베이스
    코딩테스트
    geeknews
    프로그래머스
    코테
    FE
    독학
    개발
    매일메일
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
gitit
React Quill Text Editor 도입 후 이슈 해결하기
상단으로

티스토리툴바