[북잡 프로젝트] 기존 에디터를 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 대표적인 특징
모듈화된 확장 시스템
기본적으로 글자 굵게, 기울임, 리스트 같은 기능이
StarterKit
이라는 확장으로 들어있고필요에 따라 이미지, 테이블, 링크 등 다양한 확장을 추가해서 기능을 커스터마이징 가능
🚀 사용방법
일단 나는 세가지 확장프로그램을 사용했다.
StarterKit
- 기본적인 텍스트 편집 기능을 제공 (굵게, 기울임, 리스트, 제목 등)
Image
- 이미지를 에디터에 삽입할 수 있게 해주는 확장
Placeholder
☝🏻 적용해 보았습니다
useEditor
는 Tiptap 에디터 인스턴스를 만드는 훅
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에 업로드하니 속도도 더 빠름
덕분에 클라이언트와 서버 구조, 비용과 보안 측면에서 효율적인 방식을 배울 수 있었다.
Subscribe to my newsletter
Read articles from 송수빈 directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
