Rich Text Editor

Rich Text Editor (리치 텍스트 에디터)

Rich Text Editor는 텍스트 콘텐츠를 시각적으로 작성하고 포맷하는 데 사용하는 컴포넌트예요. Tiptap을 기반으로 만들어졌어요.

출처: 문서

본문

시작하기 (Getting Started)

스니펫 추가 (Add the snippet)

리치 텍스트 에디터는 스니펫으로 제공되어 프로젝트에 추가할 수 있어요.

npx @chakra-ui/cli snippet add rich-text-editor

Tiptap StarterKit

핵심 에디터 기능을 시작하려면 Tiptap StarterKit을 설치하세요.

npm i @tiptap/starter-kit

추가 확장 (Additional extensions)

Tiptap은 에디터에 추가 기능을 붙일 수 있는 다양한 확장을 제공해요. 가장 자주 쓰는 추가 확장은 다음과 같아요:

  • Subscript: @tiptap/extension-subscript
  • Superscript: @tiptap/extension-superscript
  • Text Align: @tiptap/extension-text-align
  • Text Style: @tiptap/extension-text-style
npm i @tiptap/extension-subscript @tiptap/extension-superscript @tiptap/extension-text-align @tiptap/extension-text-style

사용법 (Usage)

import { Control, RichTextEditor } from "@/components/ui/rich-text-editor"
import { useEditor } from "@tiptap/react"
<RichTextEditor.Root editor={editor}>
  <RichTextEditor.Toolbar>
    <RichTextEditor.ControlGroup>
      <Control.Bold />
      <Control.Italic />
      <Control.Underline />
    </RichTextEditor.ControlGroup>
  </RichTextEditor.Toolbar>
  <RichTextEditor.Content />
</RichTextEditor.Root>

예시 (Examples)

편집 모드 전환 (Toggle Edit Mode)

useEditor 훅에서 editable 속성을 지정해 에디터의 모드를 제어할 수 있어요. false로 설정하면 에디터는 보기 전용 모드가 돼요.

Controlled

useEditor 훅에서 content와 onUpdate 속성을 설정해 에디터의 콘텐츠를 프로그래밍 방식으로 제어할 수 있어요.

const [content, setContent] = useState("<p>Edit here...</p>")

const editor = useEditor({
  content,
  onUpdate({ editor }) {
    setContent(editor.getHTML())
  },
})

플레이스홀더 (Placeholder)

에디터에 플레이스홀더를 추가하려면 @tiptap/extension-placeholder 확장을 사용하고 placeholder 속성을 설정하세요.

const editor = useEditor({
  extensions: [
    // ... other extensions
    Placeholder.configure({
      placeholder: "Start typing your content here...",
    }),
  ],
})

문자 수 세기 (Character Count)

실시간 문자·단어 수를 표시하려면 @tiptap/extensions/character-count 확장을 사용하세요. 특히 글자 수 제한이나 단어 수 규정이 있는 에디터에 유용해요.

const editor = useEditor({
  extensions: [
    // ... other extensions
    CharacterCount.configure({
      limit: 1000,
      mode: "textSize",
    }),
  ],
})

라이브 미리보기 (Live Preview)

에디터의 getHTML() 메서드를 사용해 콘텐츠를 가져와 읽기 전용 패널에 표시할 수 있어요.

텍스트 하이라이트 (Text Highlight)

텍스트 하이라이트를 추가하려면 @tiptap/extension-highlight 확장을 사용하고 multicolor 속성을 설정하세요. 이를 통해 <Control.Highlight /> 컴포넌트로 하이라이트 색상을 고르거나 순환할 수 있어요.

버블 메뉴 (Bubble Menu)

Tiptap의 BubbleMenu 컴포넌트를 기존 컨트롤과 함께 사용하세요. 텍스트를 선택하면 메뉴가 나타나 맥락에 맞는 포맷 옵션을 제공해요.

자동 저장 (Autosave)

에디터의 onUpdate 메서드를 사용해 자동 저장 기능을 구현할 수 있어요. 콘텐츠 변경을 처리해 서버, 로컬 스토리지, 또는 다른 영속 계층에 저장할 수 있어요.

작업 목록 (Task List)

대화형 작업 목록을 추가하려면 @tiptap/extension-task-item과 @tiptap/extension-task-list 확장을 사용하고 nested 속성을 설정하세요.

코드 블록 (Code Blocks)

@tiptap/extension-code-block-lowlight과 lowlight를 사용해 구문 강조된 코드 블록을 추가하고 원하는 언어를 하이라이트할 수 있어요.

드래그 핸들 (Drag Handle)

드래그 앤 드롭 재정렬을 추가하려면 @tiptap/extension-drag-handle-react를 사용하세요. 이 확장은 각 블록에 드래그 가능한 핸들을 제공해 사용자가 콘텐츠를 쉽게 재정렬할 수 있어요.

이미지 (Images)

이미지를 추가하려면 @tiptap/extension-image 확장을 사용하세요. 이미지 URL을 삽입하거나, 파일을 업로드하거나, 커스텀 미디어 서비스를 통합할 수 있어요.

해시태그 (Hashtags)

에디터에서 해시태그를 지원하려면 커스텀 Tiptap node를 만드세요. 해시태그를 구조화된 인라인 콘텐츠로 파싱·렌더링·처리할 수 있어요.

멘션 (Mentions)

@ 에서 트리거되도록 커스텀 Tiptap 확장을 만들고, 제공된 메뉴 컴포넌트를 사용해 추천 메뉴를 렌더링하는 멘션 예시예요.

이모지 (Emojis)

Tiptap의 Emoji 확장을 사용해 이모지 추천 기능으로 에디터를 향상시킬 수 있어요. :를 입력하거나 :), <3 같은 일반적인 이모티콘으로 이모지를 트리거할 수 있어요.

슬래시 명령어 (Slash Commands)

/ 에서 트리거되는 Tiptap 확장을 만들어 에디터에서 슬래시 명령어를 활성화할 수 있어요.

컴포지션 (Composition)

접이식 문서 개요, 스티키 툴바, 플로팅 링크 메뉴, 그리고 제목·목록·링크·이미지·텍스트 포맷용 통합 컨트롤을 갖춘 전체 페이지 에디터를 보여주는 실제 Google Docs 스타일 레이아웃이에요.

가이드 (Guides)

컨트롤 추가 (Adding controls)

RichTextEditor는 RichTextEditor.ControlGroup 안에서 구성할 수 있는 내장 컨트롤 세트를 제공해요.

import { Control } from "@/components/ui/rich-text-editor"
<RichTextEditor.ControlGroup>
  <Control.Bold />
  <Control.Italic />
  <Control.Strike />
</RichTextEditor.ControlGroup>

콘텐츠 패딩 커스터마이징 (Customizing Content Padding)

에디터는 콘텐츠 패딩에 CSS 커스텀 속성을 사용해요:

<RichTextEditor.Root
  editor={editor}
  css={{
    "--content-padding-x": "spacing.8",
    "--content-padding-y": "spacing.6",
    "--content-min-height": "sizes.96",
  }}
>
  <RichTextEditor.Content />
</RichTextEditor.Root>

커스텀 컨트롤 (Custom Controls)

RichTextEditor는 에디터와 자연스럽게 통합되는 커스텀 컨트롤을 만드는 세 가지 팩토리 함수를 제공해요: createBooleanControl, createSelectControl, createSwatchControl.

Boolean Controls

Boolean 컨트롤은 에디터 상태(볼드, 이탤릭 등)를 토글하며 가장 흔한 컨트롤 타입이에요:

import { createBooleanControl } from "@/components/ui/rich-text-editor"
import { LuSparkles } from "react-icons/lu"

export const CustomHighlight = createBooleanControl({
  label: "Highlight Important",
  icon: LuSparkles,
  command: (editor) => {
    editor
      .chain()
      .focus()
      .toggleMark("textStyle", {
        backgroundColor: "#fef08a",
        fontWeight: "bold"
      })
      .run()
  },
  getVariant: (editor) => {
    const attrs = editor.getAttributes("textStyle")
    return attrs.backgroundColor === "#fef08a" ? "subtle" : "ghost"
  },
  isDisabled: (editor) => !editor.can().toggleMark("textStyle")
})

// Use it in your toolbar
<RichTextEditor.ControlGroup>
  <CustomHighlight />
</RichTextEditor.ControlGroup>

Select Controls

Select 컨트롤은 여러 옵션 중 하나를 고르는 드롭다운 메뉴를 제공해요:

import { createSelectControl } from "@/components/ui/rich-text-editor"

export const LineHeight = createSelectControl({
  label: "Line Height",
  width: "100px",
  placeholder: "Normal",
  options: [
    { value: "normal", label: "Normal" },
    { value: "1.5", label: "1.5" },
    { value: "2", label: "Double" },
    { value: "2.5", label: "2.5" },
  ],
  getValue: (editor) => {
    return editor.getAttributes("textStyle")?.lineHeight || "normal"
  },
  command: (editor, value) => {
    if (value === "normal") {
      editor.chain().focus().unsetMark("textStyle").run()
    } else {
      editor.chain().focus().setMark("textStyle", { lineHeight: value }).run()
    }
  },
  renderValue: (value, option) => {
    return <Box fontWeight="medium">{option?.label || "Normal"}</Box>
  },
})

Swatch Controls

Swatch 컨트롤은 미리 정의된 색상 견본이 있는 컬러 피커 인터페이스를 제공해요:

import { createSwatchControl } from "@/components/ui/rich-text-editor"
import { LuPaintbrush } from "react-icons/lu"

export const BackgroundColor = createSwatchControl({
  label: "Background Color",
  icon: LuPaintbrush,
  swatches: [
    { value: "#fef3c7", color: "#fef3c7", label: "Yellow" },
    { value: "#dbeafe", color: "#dbeafe", label: "Blue" },
    { value: "#dcfce7", color: "#dcfce7", label: "Green" },
    { value: "#fce7f3", color: "#fce7f3", label: "Pink" },
  ],
  getValue: (editor) => {
    return editor.getAttributes("textStyle")?.backgroundColor || ""
  },
  command: (editor, color) => {
    editor
      .chain()
      .focus()
      .setMark("textStyle", { backgroundColor: color })
      .run()
  },
  getProps: (editor) => ({
    variant: editor.getAttributes("textStyle")?.backgroundColor
      ? "subtle"
      : "ghost",
  }),
  showRemove: true,
  onRemove: (editor) => {
    editor
      .chain()
      .focus()
      .updateAttributes("textStyle", { backgroundColor: null })
      .run()
  },
})

더 알아보기 (Learn more)