Skip to main content

Command Palette

Search for a command to run...

[북잡 프로젝트] 기존 에디터를 Tiptap으로 리팩토링한 이유와 과정

Updated
3 min readView as Markdown
[북잡 프로젝트] 기존 에디터를 Tiptap으로 리팩토링한 이유와 과정

이전에 작성한 글에서 분명 Draft.js를 사용한 이유에 대해 작성했으며 겪었던 트러블슈팅 내용까지 적었지만 결국 Tiptap으로 리팩토링을 하게 되었다..😅

그래서 이번 글에서는

  • 변경한 이유

  • 사용 방법

에 대해 적어보려고 한다.

변경한 이유

이전 글에서도 썼지만, Draft.js는 이제 유지보수가 안 되고 있다.
이 부분이 초반에는 문제가 안됐지만, 현재는 이미지 삽입 기능을 구현하는데 어려움이 있구나를 느꼈다.

문서에는 있는 것 같지만 막상 써보면 버그도 많고 커스터마이징도 복잡해서 너무 불편해서,,, ☹️ 결과적으로 tiptap으로 마이그레이션을 결정하게 되었다.


✏️ 사용방법 ( +S3 이미지 업로드)

✨ 흐름

  • 사용자가 이미지를 올리면

  • 클라이언트가 서버에 POST로 파일 정보 보내서 presigned URL과 fileId를 받음

  • 클라이언트가 presigned URL로 직접 S3에 PUT 방식으로 이미지 업로드

  • 서버는 fileId로 DB에 파일 정보를 저장(이미 처리 완료된 상태)

  • 클라이언트는 fileId를 모아두었다가, 글 작성 시 같이 서버에 보내서 어떤 이미지가 글에 포함됐는지 알려줌

  • 에디터에는 S3에 올라간 이미지의 공개 URL을 삽입해서 사용자에게 보여줌

→ presigned URL을 써서 서버 부하 줄이면서 안전하게 S3에 이미지 업로드하는 방법으로 구성 !

🔗 tiptap 대표적인 특징

  • 모듈화된 확장 시스템

    1. 기본적으로 글자 굵게, 기울임, 리스트 같은 기능이 StarterKit이라는 확장으로 들어있고

    2. 필요에 따라 이미지, 테이블, 링크 등 다양한 확장을 추가해서 기능을 커스터마이징 가능

🚀 사용방법

  • 일단 나는 세가지 확장프로그램을 사용했다.

    1. StarterKit

      • 기본적인 텍스트 편집 기능을 제공 (굵게, 기울임, 리스트, 제목 등)
    2. Image

      • 이미지를 에디터에 삽입할 수 있게 해주는 확장
    3. Placeholder

☝🏻 적용해 보았습니다

  • useEditorTiptap 에디터 인스턴스를 만드는 훅
const editor = useEditor({
  extensions: [
    StarterKit,
    Image,
    Placeholder.configure({
      placeholder: '내용을 입력하세요...',
    }),
  ],
  content: initialContent,
  onUpdate: ({ editor }) => {
    onChange?.(editor.getHTML())
  },
  editorProps: {
    attributes: {
      class:
        'tiptap prose prose-lg min-h-[300px] border border-gray-300 rounded-md p-4 focus:outline-none bg-white',
    },
    handlePaste: async (view, event) => { ... }, // 이미지 붙여넣기 처리
  },
})

useEditor를 사용해 Tiptap 에디터 인스턴스를 생성하고 기본적인 텍스트 기능, 이미지 삽입, 플레이스홀더를 확장으로 설정했다. 에디터에서 변경이 발생하면 onUpdate를 통해 HTML 형태로 부모 컴포넌트에 전달한다.

  • MenuBar에서 에디터 조작(툴바)
    <div className='mb-2 flex gap-2'>
      <button
        className='px-2 py-1 border rounded hover:bg-gray-100 font-bold'
        onClick={() => editor.chain().focus().toggleBold().run()}
        style={{ fontWeight: editor.isActive('bold') ? 'bold' : 'normal' }}
      >
        Bold
      </button>
      <button
        className='px-2 py-1 border rounded hover:bg-gray-100 italic'
        onClick={() => editor.chain().focus().toggleItalic().run()}
        style={{ fontStyle: editor.isActive('italic') ? 'italic' : 'normal' }}
      >
        Italic
      </button>
      <button
        className='px-2 py-1 border rounded hover:bg-gray-100'
        onClick={() => document.querySelector('#file-input').click()}
      >
        Image
      </button>
    </div>
  • Bold: 텍스트 굵게 (toggleBold)

  • Italic: 텍스트 기울임 (toggleItalic)

  • Image: 이미지 업로드를 위한 파일 선택창 열기

버튼의 스타일은 editor.isActive()를 활용하여 현재 적용 상태에 따라 동적으로 바뀐다.

  • BubbleMenu : 컨텍스트 기반 툴바

사용자가 이미지를 선택했을 때 선택한 이미지 위에 cancel(X) 버튼이 포함된 버블 메뉴가 나타나도록 해서 이미지를 직접 삭제할 수 있게 구현했다.

import { BubbleMenu } from '@tiptap/react'
import { FaTimes } from 'react-icons/fa'

const ImageBubbleMenu = ({ editor }) => {
  if (!editor) return null

  return (
    <BubbleMenu
      editor={editor}
      shouldShow={({ editor }) => editor.isActive('image')}
      tippyOptions={{
        duration: 100,
        placement: 'top',
        offset: [0, -10],
      }}
    >
      <button
        type='button'
        onClick={(e) => {
          e.preventDefault()
          editor.chain().focus().deleteSelection().run()
        }}
        className='bg-white text-red-500 hover:text-red-700 border border-gray-300 rounded-full w-6 h-6 flex items-center justify-center shadow-md'
        title='이미지 삭제'
      >
        <FaTimes size={12} />
      </button>
    </BubbleMenu>
  )
}

export default ImageBubbleMenu

완료 👍🏻 ( TMI: 요즘 오죠갱 너무 좋다..)

🐥 배운 점 & 느낀 점

이전 프로젝트에서는 백엔드가 PUT/POST를 직접 관리했는데 지금은 백엔드가 presigned URL만 생성해주고 클라이언트가 직접 S3에 PUT 요청하는 방식으로 바껴서 .. 신기했다 ✍🏻

백엔드 분이 이 방식이 더 많이 쓰인다고 피드백을 해줬는데.. 그 이유에 대해서 생각해봤다.. 😎

  • 백엔드 서버 부담↓ (파일 전송/처리 안 해도 됨)

  • 네트워크 비용↓ (백엔드→S3 전송 불필요)

  • 보안↑ (백엔드 서버가 직접 파일 취급 안 함)

  • 클라이언트가 직접 S3에 업로드하니 속도도 더 빠름

덕분에 클라이언트와 서버 구조, 비용과 보안 측면에서 효율적인 방식을 배울 수 있었다.

More from this blog

[React] 클라이언트에서 이미지를 압축해보자

우리는 서버 비용을 직접 부담하고 있기 때문에 항상 최적화와 비용 절감 방법을 고민하게 된다. 현재 자유게시판이 활성화가 많이 되지는 않았지만 사용자들이 업로드하는 이미지가 평균 5MB 이상일 경우를 대비해서 클라이언트에서 이미지 압축을 진행했다. 웹에서는 크게 두 가지 방법이 있었는데 Canvas API를 직접 사용하는 방법 browser-image-compression 라이브러리를 활용하는 방법 이 있다. 여러 장점을 고려해 brow...

Oct 8, 20252 min read
[React] 클라이언트에서 이미지를 압축해보자

[책 추천] 프론트엔드 개발자라면 반드시 알아야 할 '웹 접근성' 이야기

내가 생각하는 프론트엔드 개발자는 단순히 보이는 화면만 구현하는 것을 넘어서 누구나 접근할 수 있는 웹을 만드는 데 중요한 역할을 해야 한다고 생각한다. 실무에 바로 적용하는 웹 접근성 가이드북 이번에 이 책을 받고 프엔들이 읽기에 너무 좋다고 생각해서 블로그에 추천 글까지 적게 되었다 ! 목차 Chatper1. 쉽게 이해하는 접근성 Chapter2. 웹 접근성의 기초 Chapter3. HTML 태그, 의미 있게 사용하기 Chpater4...

Jul 27, 20255 min read
[책 추천] 프론트엔드 개발자라면 반드시 알아야 할 '웹 접근성' 이야기

[Next.js] 나만의 학습 블로그 만들기#3-다국어 지원 (Feat.next-i18next)

오늘은 다국어 지원 가능한 기능 구현 과정을 적어보려고 한다. 원래는 댓글기능보다 먼저 구현하려고 했는데 ,, 갑자기 댓글기능 알아보다가 재밌어서 먼저 끝내버렸다,,,🙄 시작하기 전에 정말 어려웠다 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 어려움이 많아서 기능 적용하는 데 시간이 꽤 걸렸다 ,,, 다국어 지원 기능 나는 영어랑 일본어를 추가해줬다 ! (해외취업도 관심 있기 때문에 나중에 이력서 낼 때 도움이 되지 않을까? ..🤭) 다국어 구현 방법 구현 방...

Jul 15, 20254 min read
[Next.js] 나만의 학습 블로그 만들기#3-다국어 지원 (Feat.next-i18next)

[Next.js] 나만의 학습 블로그 만들기#4 - 댓글 기능(Feat. Giscus)

댓글기능이 있으면 무조건 재밌을 거 같아서 넣어보려고 한다🤭 아무래도 개발에 관련된 학습 블로그라서 개발자들이 많을 것으로 예상했다 ! 그래서 깃허브 이슈 기반 댓글 시스템을 기반으로 기능을 구현해보려고 한다. 👉🏻 도움이 많이 된 블로그 ! 방법 Giscus | (https://giscus.app) utterances | (https://utteranc.es) 알아봤을 때는 이렇게 두 개가 유명하다고 한다. 그럼 각각의 특징과 장...

Jul 14, 20253 min read
[Next.js] 나만의 학습 블로그 만들기#4 - 댓글 기능(Feat. Giscus)

[React] React + localStorage로 하루에 한 번만 보이는 팝업 만들기

이번 프로젝트에 설문조사 배너를 제작했는데… 흠… 뭔가 별로라는 의견이 많았다! (팀원들 의견) 🙄 디자인이 문제일까? 해서 래퍼런스를 많이 찾아봤는데 … ! 일반 웹사이트는 설문조사 배너 자체를 안 만들어요,,,🤭 충격 그 자체 ( 난 아직 멀었음 ) 그리고 알아보니깐.. 설문조사 배너는 사용자 경험을 해치기 쉬워 잘 쓰이지 않는다고 한다! 그래서 나는 모달 팝업 방식으로 전환하기로 결정했고 실무에서 자주 사용하는 패턴으로 구현했다. 방...

Jul 9, 20253 min read
[React] React + localStorage로 하루에 한 번만 보이는 팝업 만들기
S

subin-dev-blog

27 posts

끊임없이 배우기 위해 노력합니다. 🌱