본문 바로가기
Utility

텍스트 에디터 Tiptap 커스터마이징

by seyoonagain 2025. 1. 19.

이번 프로젝트 때 텍스트 에디터를 사용해볼 기회가 생겼당!

처음엔 quill을 쓰려고 했지만 next.js 15버전과의 호환성 때문인지 내가 못하는 건지 @.@ 

quill, quill-react, quill-react-new를 다 시도해 보았지만 커스터마이징에 실패

 

다른 팀에서 TipTap을 사용한다고 해서 나도 Tiptap으로 갈아탔당 🚌


🔎 미리보기

Tiptap은 next.js 15버전에서 아래와 같이 아주 잘 작동하였당.

아이콘은 lucide 라이브러리를 사용했당.

컬러팔레트까지 커스터마이징이 가능해서 컬러팔레트까지 이번 프로젝트 주요 색상을 이용해 만들었당.

사진도 잘 들어간당.

 

 

모바일일 땐 undo와 redo 버튼을 없애고, 헤딩 옵션을 하나의 아이콘으로 합쳐 선택지가 드롭다운처럼 뜨도록 만들었다.


🛠️ Tiptap 설치

 

Install the Editor | Tiptap Editor Docs

Integrate the Tiptap editor into your project. Tiptap is framework-agnostic, offering full compatibility with most frameworks.

tiptap.dev

npm install @tiptap/react @tiptap/pm @tiptap/starter-kit
yarn add @tiptap/react @tiptap/pm @tiptap/starter-kit
pnpm add @tiptap/react @tiptap/pm @tiptap/starter-kit

🔨 텍스트 에디터 만들기

나는 Tiptap 관련 로직을 분리시키고자 훅으로 따로 뺐다.

훅 내부에서 useEditor를 통해 텍스트 에디터를 만들었다.

이 때 에디터 내에서 사용할 기능들을 extensions 안에서 정의할 수 있다.

   const editor = useEditor({
    extensions: [
      StarterKit.configure({
        heading: false,
      }),
      Underline,
      Image,
      Color,
      TextStyle.configure({ mergeNestedSpanStyles: true }),
      Highlight.configure({ multicolor: true }),
      Heading.configure({
        levels: [1, 2, 3],
      }),
      CharacterCount.configure({ limit: LIMIT }),
    ],
    onCreate: ({ editor }) => {
      // 에디터 실행 시 동작
    },
    onUpdate: ({ editor }) => {
      // 에디터에 입력이 있을 때마다 동작
    },
    immediatelyRender: false,
    content: '', // 텍스트 에디터 초기 텍스트 값
  });

 

처음 설치할 때 함께 설치한 StarterKit에는 기본적으로 아래의 기능들이 있고,

그 외에 추가 기능을 사용하고자 한다면 extension을 별도로 설치하여 사용할 수 있다.

 

StarterKit에 기본적으로 Heading 기능이 있었지만 추가 설정이 필요하여, 별도로 설치하였다.

StarterKit에 있는 기능이지만 별도로 설치 후 추가 설정을 하는 경우,

아래와 같이 StarterKit의 해당 기능을 사용하지 않겠다는 코드를 작성해주어야 한다.

StarterKit.configure({
        heading: false,
      })

 

내가 추가로 설치한 extension들

  • Heading: <h1> ... <h6> 태그 사용을 위한 extension
  • Color, Highlight, TextStyle
    • Color: 글자 색상
    • Highlight: 글자 배경 색상
    • TextStyle: span 태그를 이용해 글자 스타일을 적용시킬 수 있게 도와주는 extension
  • Underline: 밑줄
  • Image: 사진 추가
  • CharacterCount: 글자 수 제한

StarterKit에 있는 것을 그대로 사용한 기능

  • Bold
  • Italic
  • Strike
  • ListItem
  • History

✨ 텍스트 에디터 렌더링 ✨

나는 툴바와 에디터를 별도의 컴포넌트로 만들었고,

두 컴포넌트를 포함하는 상위 컴포넌트에서 useTiptap 훅을 호출하여 editor를 가져와 툴바와 에디터에 editor 객체를 전달하였다.

import useTiptap from '@/hooks/useTiptap';

const Form = () => {
  const { editor } = useTiptap()
    
  return (
    <>
      <Toolbar editor={editor} />
      <Tiptap editor={editor} />
    </>
  )
}

대략 위와 같은 식으루...

실제 코드는 다른 코드들이 많아 지저분해서 관련 부분만 적어봄 ^_^...

 

Tiptap 컴포넌트 내에서는 EditorContent를 이용하여 실제 텍스트 에디터를 렌더링한다.

텍스트 에디터의 전체적인 사이즈 및 스타일을 className을 통해 설정해주었다.

import { EditorContent } from '@tiptap/react';
import { TiptapProps } from '../types';

const Tiptap = ({ editor }: TiptapProps) => {
  return (
    <>
      <EditorContent
        editor={editor}
        className="w-full h-[640px] border border-t-0 border-green rounded-b-2xl bg-white overflow-scroll md:h-[768px]"
      />
    </>
  );
};

🚨 Heading, ListItem이 적용이 안되는 문제 발생

Tailwind CSS를 사용하다보니 일부 reset CSS가 적용되어 에디터 내부에서는 태그가 적용되었으나,

화면상 스타일이 나타나지 않는 문제 발생

 

💡 ClassName에 Tailwind CSS 코드를 추가함으로써 해결

configure 메소드의 levels 키를 통해 사용할 Heading 태그의 레벨을 지정하고,

extend에서 태그명, 태그 속성 등을 지정할 수 있다.

class에 Tailwind CSS 코드를 작성함으로써 Heading 태그의 스타일을 설정할 수 있었당!

const HEADING_CLASSES: Record<Level, string> = {
  1: 'text-2xl',
  2: 'text-xl',
  3: 'text-lg',
};
Heading.configure({
        levels: [1, 2, 3],
      }).extend({
        renderHTML({ node }) {
          const level: Level = node.attrs.level;
          const baseClass = 'font-bold';
          const sizeClass = HEADING_CLASSES[level] || 'text-base';
          return [`h${level}`, { class: `${baseClass} ${sizeClass}` }, 0];
        },
      }),

 

💡 globals.css에서 스타일 설정하기

ul, ol, li 태그의 경우는 reset된 스타일을 globals.css에서 아래와 같이 다시 설정해주었다.

ol > li {
  @apply list-decimal pl-2;
}

ul > li {
  @apply list-disc pl-2;
}

ol,
ul {
  @apply pl-5;
}

 

 

‼️ 입력창이 작아요!

분명 EditorContent에서 높이를 지정해주었음에도,

텍스트 에디터에 포커싱 되었을 때 다음과 같이 못생긴 아웃라인이 겨우 한 줄에 해당하는 옹졸한 높이의 입력창을 가지고 있음을 보여준당.

 

💡 입력창 크기 조절함으로써 해결

위의 문제를 해결하기 위해, useEditor에 전달하는 객체에서 아래와 같은 옵션을 추가한다.

    editorProps: {
      attributes: {
        class: 'h-full p-4 prose prose-sm sm:prose-base lg:prose-lg xl:prose-2xl',
      },
    },

 

h-full을 적용시킴으로써 아래처럼 아웃라인의 크기가 달라진 걸 알 수 있다.

 

🤢 못생긴 아웃라인

outline-0 outline-none을 아무리 적용해도 사라지지 않는 아웃라인...

.ProseMirror-focused {
  @apply outline-none;
}

 

내부적으로 에디터에 적용된 스타일이 있는 듯하당.

위와 같이 globals.css 파일 내에서 해당 클래스 네임에 해당하는 스타일을 다시 설정함으로써

아웃라인을 없애고 깔끔한 에디터를 얻을 수 있다 ^________________^


🌝 최종 코드

 useTiptap.ts 

   const editor = useEditor({
    extensions: [
      StarterKit.configure({
        heading: false,
      }),
      Underline,
      Image,
      Color,
      TextStyle.configure({ mergeNestedSpanStyles: true }),
      Highlight.configure({ multicolor: true }),
      Heading.configure({
        levels: [1, 2, 3],
      }).extend({
        renderHTML({ node }) {
          const level: Level = node.attrs.level;
          const baseClass = 'font-bold';
          const sizeClass = HEADING_CLASSES[level] || 'text-base';
          return [`h${level}`, { class: `${baseClass} ${sizeClass}` }, 0];
        },
      }),
      CharacterCount.configure({ limit: LIMIT }),
    ],
    editorProps: {
      attributes: {
        class: 'h-full p-4 prose prose-sm sm:prose-base lg:prose-lg xl:prose-2xl',
      },
    },
    onCreate: ({ editor }) => {
      // 에디터 실행 시 동작
    },
    onUpdate: ({ editor }) => {
      // 에디터에 입력이 있을 때마다 동작
    },
    immediatelyRender: false,
    content: '', // 텍스트 에디터 초기 텍스트 값
  });

 

 globals.css 

.ProseMirror-focused {
  @apply outline-none;
}

ol > li {
  @apply list-decimal pl-2;
}

ul > li {
  @apply list-disc pl-2;
}

ol,
ul {
  @apply pl-5;
}

툴바까지 작성하고 싶었지만

정처기 공부해야 해서 이만 총총총!

'Utility' 카테고리의 다른 글

useWebSocket으로 웹소켓 구현하기  (1) 2025.01.14
돌아가는 로딩 스피너 코드  (0) 2024.12.23
cva (Class Variance Authority)  (1) 2024.12.14
clsx  (0) 2024.12.13
Tailwind Merge  (0) 2024.12.13