이번 프로젝트 때 텍스트 에디터를 사용해볼 기회가 생겼당!
처음엔 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 |