React에 TipTap 붙이기

Sei·2025년 10월 20일
post-thumbnail

툴바부터 테마까지 내가 원하는 대로

TipTap은 디자인 자유도가 높은 리치 텍스트 에디터로
오늘은 아주 유용한 TipTap에 대해서 이야기 해보려고 한다!

이 글에서 다룰 것

  1. TipTap을 쓰면 뭐가 좋은지 한 줄 정리

  2. React에서 설치 → 최소 예제 → 저장/불러오기까지

  3. 툴바 버튼 직접 만들어 보기(볼드/이탤릭/헤딩)

  4. 스타일링 팁과 Next.js(SSR) 주의점

  5. 자주 겪는 이슈 Q&A

  6. 왜 TipTap일까?

  • 헤드리스(Headless):
    “편집기 로직”만 있고, UI는 전부 내가 만든다
    → 디자인 시스템(Tailwind, shadcn/ui 등)과 찰떡궁합!

  • 확장(Extension) 기반: 필요한 기능만 쏙쏙 무겁지 않게

  • ProseMirror 위라서 안정적이고 커스터마이징 폭이 큼

“회사/프로젝트의 룩앤필을 해치지 않고, 에디터를 내 디자인처럼”

설치 (3줄 컷)

npm create vite@latest my-tiptap -- --template react-ts
cd my-tiptap
npm i @tiptap/react @tiptap/pm @tiptap/starter-kit

최소 예제

// src/Tiptap.tsx
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

export default function Tiptap() {
  const editor = useEditor({
    extensions: [StarterKit],
    content: '<p>Hello TipTap! <strong>Bold</strong> 테스트</p>',
  })

  if (!editor) return null
  return <EditorContent editor={editor} />
}

// src/App.tsx
import Tiptap from './Tiptap'

export default function App() {
  return (
    <main style={{ maxWidth: 720, margin: '40px auto' }}>
      <h1>My TipTap Editor</h1>
      <Tiptap />
    </main>
  )
}

여기까지가 “보여주기”의 끝!
이제부터 “쓰기 편하게 만들기”를 해보자

가장 많이 쓰는 툴바 3종 세트 만들기

TipTap은 버튼을 기본 제공하지 않는다.
대신 editor.chain().focus().toggleBold().run() 같은 명령을 호출하면 끝!

// src/Toolbar.tsx
import { useCurrentEditor } from '@tiptap/react'

export default function Toolbar() {
  const { editor } = useCurrentEditor()
  if (!editor) return null

  const Button = (props: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
    <button
      {...props}
      style={{
        padding: '6px 10px',
        borderRadius: 8,
        border: '1px solid #ddd',
        background: '#fff',
        cursor: 'pointer',
      }}
    />
  )

  return (
    <div style={{ display: 'flex', gap: 8, margin: '12px 0' }}>
      <Button
        onClick={() => editor.chain().focus().toggleBold().run()}
        style={{ fontWeight: editor.isActive('bold') ? 700 : 400 }}
      >
        B
      </Button>
      <Button
        onClick={() => editor.chain().focus().toggleItalic().run()}
        style={{ fontStyle: editor.isActive('italic') ? 'italic' : 'normal' }}
      >
        I
      </Button>
      <Button onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}>
        H2
      </Button>
      <Button onClick={() => editor.chain().focus().setParagraph().run()}>
        P
      </Button>
      <Button onClick={() => editor.chain().focus().undo().run()}>
        Undo
      </Button>
      <Button onClick={() => editor.chain().focus().redo().run()}>
        Redo
      </Button>
    </div>
  )
}
// src/Tiptap.tsx (툴바 붙이기)
import { useEditor, EditorContent, EditorContentProps, Editor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import Toolbar from './Toolbar'

export default function Tiptap() {
  const editor = useEditor({
    extensions: [StarterKit],
    content: '<p>여기에 글을 입력해보세요 ✍️</p>',
  })

  if (!editor) return null
  return (
    <>
      <Toolbar />
      <EditorContent editor={editor} className="tiptap" />
    </>
  )
}

저장/불러오기(로컬 저장소 기준)

TipTap은 JSON/HTML 둘 다 지원한다.
보통은 JSON 저장이 구조를 보존하기 좋다!

// src/SaveLoad.tsx
import { useCurrentEditor } from '@tiptap/react'

export default function SaveLoad() {
  const { editor } = useCurrentEditor()
  if (!editor) return null

  const save = () => {
    const json = editor.getJSON()
    localStorage.setItem('post', JSON.stringify(json))
    alert('저장 완료!')
  }

  const load = () => {
    const raw = localStorage.getItem('post')
    if (!raw) return alert('저장된 내용이 없어요')
    editor.commands.setContent(JSON.parse(raw))
  }

  return (
    <div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
      <button onClick={save}>저장</button>
      <button onClick={load}>불러오기</button>
    </div>
  )
}
// src/Tiptap.tsx (저장/불러오기 UI 포함)
import SaveLoad from './SaveLoad'
// ...
return (
  <>
    <Toolbar />
    <SaveLoad />
    <EditorContent editor={editor} className="tiptap" />
  </>
)

스타일링 팁 (진짜 중요)

TipTap은 스타일을 거의 안 주고 보낸다.
그래서 최소한 아래 정도는 잡아두면 보기 좋아집니다.

/* src/index.css */
.tiptap {
  min-height: 220px;
  padding: 16px;
  border: 1px solid #e5e7eb;
  border-radius: 12px;
  line-height: 1.7;
  background: #fff;
}

/* 본문 요소들 */
.tiptap p { margin: 0 0 0.8em; }
.tiptap h1, .tiptap h2, .tiptap h3 { font-weight: 700; line-height: 1.3; margin: 1.2em 0 0.6em; }
.tiptap ul, .tiptap ol { padding-left: 1.4em; margin: 0.8em 0; }
.tiptap code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; padding: 2px 6px; border-radius: 6px; background: #f5f5f5; }
.tiptap pre { background: #0b1020; color: #e6edf3; padding: 12px 14px; border-radius: 12px; overflow: auto; }
.tiptap blockquote { border-left: 4px solid #e5e7eb; margin: 1em 0; padding-left: 12px; color: #6b7280; }

Tailwind를 쓴다면 className="prose" 같은 걸 바로 붙이지 말고, .tiptap 범위에 필요한 스타일만 얇게 깔아주는 걸 추천한다!
(툴바/테마랑 충돌 적음)

Next.js(SSR)이라면 이 옵션을 꼭 기억하자!

SSR에서는 에디터를 클라이언트에서만 그리게 해야 경고가 안 뜬다.

'use client'
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

export default function MyEditor() {
  const editor = useEditor({
    extensions: [StarterKit],
    content: '<p>SSR 안전 모드</p>',
    immediatelyRender: false, // ← 요게 포인트!
  })

  if (!editor) return null
  return <EditorContent editor={editor} className="tiptap" />
}

자주 겪는 이슈 Q&A

Q1. 왜 기본 툴바가 없죠?
A. TipTap은 헤드리스라 일부러 안 넣었다.
대신 editor.commands.*나 editor.chain()으로 버튼을 내 입맛대로 만들 수 있다.

Q2. HTML로 저장하면 안 되나요?
A. 가능합니다. 다만 “구조”를 유지해야 하거나 나중에 머지/검증을 하려면 JSON이 더 좋다. (서버에서 파싱/검증도 쉬움)

Q3. 긴 문서에서 렉이 걸려요.
A. 툴바/상태 표시 컴포넌트를 쪼개고, editor.isActive() 호출을 최소화하자! 필요하면 하이라이트/구문강조 같은 무거운 작업은 지연 처리를 고려하자.

Q4. 컴포넌트에서 editor가 자꾸 null이에요.
A. useEditor()는 초기 렌더에서 null일 수 있다.
if (!editor) return null을 기억하자!

확장(Extension) 선택 가이드 (입문 추천)

StarterKit:
문단/헤딩/리스트/코드블럭/수평선/볼드/이탤릭/취소/재실행 등 기본 풀세트

Link / Image / Table / Placeholder / Mention: 필요할 때 하나씩 추가

CodeBlockLowlight: 코드 하이라이트가 필요할 때

팁: “지금 당장 안 쓰는 기능”은 과감히 빼두자
에디터는 가볍고 빠르게 느껴지는 게 가장 중요하다.

profile
front-end developer

0개의 댓글