Skip to main content

개발을 하다 보면 사용자가 직접 글을 작성하는 기능, 즉 리치 텍스트 에디터(Rich Text Editor, WYSIWYG)가 필요한 순간이 온다. 이번에 모두의회생 프로젝트에서 기존에 사용하던 Toast UI Editor를 TipTap으로 교체하면서 겪은 과정과 선택 이유를 정리해봤다.

코드 에디터 화면

왜 에디터를 교체했나?

기존에는 Toast UI Editor를 CDN으로 불러와 사용하고 있었다. 작동은 잘 됐지만 몇 가지 불편한 점이 있었다.

  • CDN 의존성: 페이지 로드 시마다 외부 스크립트를 불러와야 해서 네트워크 비용 발생
  • SvelteKit 호환성 부족: 컴포넌트로 캡슐화하기 어렵고, SSR과 충돌
  • 마크다운 기반: 데이터베이스에 HTML로 저장하는 구조와 맞지 않음
  • 번들 포함 불가: npm 패키지로 관리가 안 돼 버전 관리가 불편

CKEditor 대신 TipTap을 선택한 이유

에디터 라이브러리는 생각보다 선택지가 많다. 주요 후보들을 비교해봤다.

에디터라이선스특징단점
CKEditor 5GPL (상업 유료)가장 완성도 높은 UI상업 사용 시 유료
QuillMIT간단하고 안정적개발 정체, 커스텀 한계
Toast UIMIT마크다운+WYSIWYG마크다운 기반, 무거움
TipTapMIT (코어)ProseMirror 기반, 헤드리스스타일 직접 작성 필요

TipTap을 선택한 결정적 이유는 헤드리스(Headless) 아키텍처다. UI를 강요하지 않고 기능만 제공하기 때문에 Tailwind CSS와 자연스럽게 조합할 수 있었다. 또한 npm 패키지로 설치해 번들에 포함시킬 수 있어 CDN 의존성을 완전히 제거할 수 있었다.

SvelteKit에서 TipTap 통합하기

TipTap은 Vue, React 공식 지원 외에도 Vanilla JS로 사용할 수 있어 SvelteKit과도 잘 동작한다. onMount에서 Editor 인스턴스를 생성하고 onDestroy에서 정리하는 패턴이다.

// RichEditor.svelte
import { onMount, onDestroy } from 'svelte';
import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';

export let value = '';
let el;
let editor;

onMount(() => {
  editor = new Editor({
    element: el,
    extensions: [StarterKit],
    content: value,
    onUpdate({ editor }) {
      value = editor.getHTML(); // bind:value 자동 동기화
    },
  });
});

onDestroy(() => editor?.destroy());

핵심은 onUpdate 콜백에서 editor.getHTML()value에 대입하는 것이다. Svelte의 bind:value와 조합하면 부모 컴포넌트에서 HTML 값을 실시간으로 받아볼 수 있다.

구현한 주요 기능들

1. 풍부한 툴바

Bold, Italic, Underline, Strike, H1~H3, 불릿/번호 리스트, 인용문, 구분선, 텍스트 정렬(좌/중/우), 링크, 이미지, 실행취소/다시실행을 모두 구현했다. 각 버튼은 TipTap의 editor.isActive() API로 현재 상태를 반영한다.

2. 이미지 파일 업로드

이미지 버튼 클릭 시 다이얼로그가 나타나며 두 가지 방식을 지원한다.

  • 파일 업로드: 클릭/드래그로 이미지 선택 → 백엔드 API로 S3 업로드 → URL 자동 삽입
  • URL 입력: 외부 이미지 URL 직접 붙여넣기

백엔드는 FastAPI로 구현했으며, S3가 설정되어 있으면 AWS S3에, 아니면 로컬 서버에 저장하는 폴백 구조를 가지고 있다.

3. WYSIWYG ↔ HTML 소스 모드 토글

CKEditor의 “소스” 버튼처럼, 툴바 우측 </> 버튼으로 두 가지 모드를 전환할 수 있다.

  • WYSIWYG 모드: TipTap 비주얼 에디터로 글 작성
  • HTML 소스 모드: 모노스페이스 폰트 textarea에서 원시 HTML 직접 편집

모드 전환 시 TipTap 에디터 DOM은 display:none으로 숨기되 유지하여, 다시 위지윅 모드로 돌아올 때 상태 손실이 없도록 했다.

function toggleHtmlMode() {
  if (!htmlMode) {
    rawHtml = editor.getHTML();
    htmlMode = true;
  } else {
    editor.commands.setContent(rawHtml, false);
    htmlMode = false;
  }
}

ToastUI에서 TipTap으로 마이그레이션

프로젝트 내 ToastUI Editor를 사용하던 페이지가 5개였다. 각 페이지에서 해야 할 작업은 다음과 같았다.

  1. svelte:head의 ToastUI CSS link 제거
  2. 동적 CDN 스크립트 로딩 코드(initToastEditor()) 제거
  3. editor.getHTML() 호출 로직 제거 (bind:value가 자동 처리)
  4. <div bind:this={editorElement}></div><RichEditor bind:value={post.content} />로 교체

수정 페이지(edit)의 경우, 기존 데이터를 불러온 후 에디터가 렌더링되어야 했다. {#key post.id} 블록으로 감싸 데이터 로드 후 에디터를 재마운트하는 방식으로 해결했다.

{#key post.id}
  <RichEditor bind:value={post.content} token={auth.token} minHeight="480px" />
{/key}

결론

TipTap은 헤드리스 + npm 설치 + HTML 출력이라는 세 가지 조건을 모두 만족하는 유일한 무료 에디터였다. 특히 SvelteKit처럼 번들러를 사용하는 환경에서는 CDN 방식보다 npm 패키지 방식이 훨씬 관리하기 쉽다.

커스텀 컴포넌트로 만들어두면 프로젝트 내 어디서든 <RichEditor bind:value={content} /> 한 줄로 바로 붙여 쓸 수 있다는 것도 큰 장점이다. 앞으로 AI 글쓰기 보조, 멘션(@ 기능), 협업 편집(Y.js) 같은 기능도 TipTap 확장으로 자연스럽게 추가할 수 있다.


이 글은 코드벤터가 실제 프로젝트에서 경험한 내용을 정리한 기술 블로그입니다. 실제 구현 코드는 모두의회생 서비스에 적용되어 있습니다.

댓글 남기기