세상에는 참 많은 라이브러리가 있다. 종류도 다양한 만큼 사용 방법도 다양하고, 그것 때문에 사실 개발을 처음 시작했을 때는 막연하게 좀 두려운 존재라고 생각했었던 것 같다. 지금이야 GPT나 Claude 같은 존재를 알게 되었지만 4년 전의 나는 오로지 구글 서칭 한 우물을 파고 있었으니....... (라이브러리를 많이 쓰면 왠지 내 실력이 오르지 않을 것 같기도 했고)
아무튼 그런 이유로 대부분 직접 구현하는 일이 많았는데, 아무리 노력해도 내 능력으로는 할 수 없는 것이 있을 때는 라이브러리를 사용해야만 했다. 대표적인 것 중 하나가 바로 텍스트 에디터였다. 기능이 한두 개도 아니고, 어디서부터 어떻게 구현해야 할지...... 지금 생각해도 정말 까마득하다. 그때 내 기억에는 Quill, CKEditor 등을 사용했던 것 같은데, 이번 프로젝트에서는 tiptap을 사용하기로 했다.
내가 tiptap 에디터를 선택한 이유는 우선 최근 많이 사용하는 라이브러리라는 점도 있고, 원하는 기능만을 설치해서 사용할 수 있기 때문에 부담이 적다는 점도 있었다. headless를 표방하기 때문에 스타일링을 마음대로 구성할 수도 있다. 게다가 사용법도 간단하고, 기능뿐만 아니라 간단한 유틸과 UI를 제공하는 컴포넌트도 설치하여 사용할 수 있다!
사용하는 방법은 여러 가지가 있지만, tiptap에서 기본적으로 제공하는 컴포넌트의 디자인이 shadcn/ui의 디자인과 크게 다르지 않아서 이번에는 컴포넌트를 설치해 사용하는 방식을 택했다. 방법만 선택하면 그 다음은 정말 어렵지 않게 작업 가능!
우선 tiptap 공식 문서에 접속하여 설치 방법을 확인한다.

필수로 @tiptap/react, @tiptap/pm을 설치하면 에디터를 사용할 수 있으며, @tiptap/starter-kit은 기본적인 텍스트 스타일(폰트 두께, 크기 등...)을 지정하게 해준다. 스타터킷을 바탕으로 해서 필요에 따라 다양한 익스텐션을 설치하여 사용할 수 있으며, 심지어 커스텀 익스텐션을 만들어서 사용할 수도 있다!
회사 내 프로젝트에서 사용해본 적이 있는데(letter-spacing을 지정하는 기능이었다), css에서 지원하는 대부분의 기능을 익스텐션으로 만들 수 있고 심지어 사용자들이 미리 만들어 놓은 익스텐션도 있어 커스텀 자유도가 무척 높은 것이 장점이다. 자세한 내용은 아래 링크를 참고!
아무튼 라이브러리를 설치했다면 이제 컴포넌트 내에서 에디터를 초기화할 차례다!
초기화 코드는 매우 간단하다.
import { useEditor } from "@tiptap/react";
import { StarterKit } from "@tiptap/starter-kit";
export const Test = () => {
const editor = useEditor({
extensions: [StarterKit],
content: "",
onUpdate: ({ editor }) => {
setIsEmpty(editor.isEmpty);
},
});
return (
<EditorContext.Provider value={{ editor }}>
<EditorContent
className="max-h-[200px] overflow-auto"
editor={editor}
/>
</EditorContext.Provider>
)
}
useEditor 훅을 통해서 에디터 인스턴스를 생성하고 extensions에 내가 설치한 익스텐션을 배열에 담아서 지정해 주면 된다. content는 에디터 내용의 초기값이고, onUpdate 핸들러에는 현재 에디터가 비어 있는지 아닌지를 판단하기 위한 스테이트를 넣어 놓았다. ...정말 간단하다! 물론 아직 '컨텐츠' 밖에 없는 상태이기 때문에 완전한 에디터로 사용하기 위해서는 몇 가지 작업이 더 필요하다.
필요한 익스텐션을 설치한 상태고, 그것을 구현하기 위한 컴포넌트를 추가적으로 설치하지 않았다면 사용자가 직접 UI를 만들어서 아래와 같이 기능을 구현하면 된다.
<button
onClick={() => editor.chain().focus().toggleBold().run()}
disabled={!editorState.canBold}
className={editorState.isBold ? 'is-active' : ''}
>
Bold
</button>
위에서 생성했던 에디터 인스턴스와, 현재 에디터에서 선택된 블럭의 다양한 스타일링 상태를 저장하고 있는 editorState를 사용하여 쉽게 간단한 기능을 구현할 수 있다. 위의 예시는 폰트의 두께를 bold/normal로 토글하는 함수다.
나는 개발의 속도를 높이기 위해서 위와 같이 직접 구현하는 대신 컴포넌트를 추가하여 사용했다. 컴포넌트를 추가하는 방식은 shadcn/ui와 매우 유사한데, npx 명령어를 실행하면 내 프로젝트 폴더에 컴포넌트와 기능을 구현하는 데에 필요한 익스텐션, 그리고 유틸 함수가 들어 있는 ts 파일이 직접 추가된다. 사용 가능한 컴포넌트는 아래 링크에서 확인할 수 있다.
tiptap 에디터 컴포넌트
참고로 가장 상단에 'Available for free'라고 적혀 있는 컴포넌트만 무료로 사용할 수 있고 그외에는 필요에 따라 결제가 필요하다. 나는 중요한 글자를 강조하기 위해 Color highlight popover 컴포넌트를 사용해 보려고 한다. 우선 터미널에 아래와 같은 명령어를 입력하여 컴포넌트를 프로젝트에 추가한다.
npx @tiptap/cli@latest add color-highlight-popover
그리고 화면에 불러와 적용한다. 새로 추가한 ColorHighlightPopover 컴포넌트와 함께 설치되었을 Hgihligt 익스텐션, 그리고 기본적인 스타일링을 도와줄 scss 파일을 함께 임포트했다. 추가한 익스텐션은 useEditor 안의 extensions에 추가하는 것을 잊지 말자! 에디터 인스턴스는 프로바이더에 의해 하위의 모든 컴포넌트에 공유된다.
import { useEditor } from "@tiptap/react";
import { ColorHighlightPopover } from "@/components/tiptap-ui/color-highlight-popover";
import { StarterKit } from "@tiptap/starter-kit";
import { Highlight } from "@tiptap/extension-highlight";
import "@/components/tiptap-node/paragraph-node/paragraph-node.scss";
export const Test = () => {
const editor = useEditor({
extensions: [StarterKit, Highlight],
content: "",
onUpdate: ({ editor }) => {
setIsEmpty(editor.isEmpty);
},
});
return (
<EditorContext.Provider value={{ editor }}>
<ColorHighlightPopover />
<EditorContent
className="max-h-[200px] overflow-auto"
editor={editor}
/>
</EditorContext.Provider>
)
}
이렇게 화면에 추가하고 나면, 사진과 같은 모습이 나타난다.

초록색 동그라미로 표시한 부분이 바로 우리가 추가한 버튼 컴포넌트! 클릭하면 팝오버 메뉴가 나타나고, 텍스트의 범위를 지정하여 적용할 수도 있다. (팁탭 에티터라는 글자를 파란색으로 강조)


컴포넌트가 잘 적용되는 것을 보았으니, 기능 구현에 필요한 MarkButton, HeadingDropdownMenu, BlockquoteButton, LinkPopover 버튼을 추가하여 에디터를 완성하면 대략 이런 느낌이다.

추가한 기능을 보여주기 위해 다양한 스타일링을 시도해 보았다! 이제 이렇게 작성한 컨텐츠를 서버에 저장해 보려고 한다. tiptap 에디터로 작성된 컨텐츠는 html 그대로를 뽑아낼 수도 있고, tiptap에서 제공하는 json 형태로 뽑아낼 수도 있다. 나는 저장된 컨텐츠를 화면에 보여줄 때도 tiptap 에디터를 사용할 것이기 때문에, 같은 라이브러리라면 호환성이 좋은 json 형태로 저장할 것이다. 단순히 저장했던 컨텐츠를 json 형태 그대로 다시 에디터에 넣어주기만 하면 된다.
먼저 '게시하기' 버튼을 눌렀을 때 json 데이터를 얻는 방법은, 에디터 인스턴스에 내장된 editor.getJSON 메서드를 호출하는 것이다. ...그게 끝이다. json 안에는 컨텐츠에 대한 설정값과 내용들이 구조화되어 저장되어 있다. 그리고 불러올 때는 이렇게!
const editor = useEditor({
extensions: [StarterKit, Highlight],
content: post.content, // 서버에 저장했던 값
editable: false,
});
작성에 사용했던 에디터에서 추가한 것과 같은 익스텐션과, 서버에서 저장했던 json을 추가해준 뒤 editable만 false로 바꾸면 완벽한 컨텐츠 렌더러가 된다 ㅎㅎ

작성했던 것과 동일한 화면이 나타나는 것을 확인할 수 있다. 만약 에디터가 초기화되는 시점에 서버 데이터가 불러와지지 않아서 간혹 에디터가 빈 화면으로 나온다면 useEffect를 활용하며 에디터의 컨텐츠를 세팅해주면 된다.
useEffect(() => {
if (post?.content) {
editor?.commands.setContent(post.content as Content);
}
}, [post?.content]);
오늘은 이렇게 간단하게 tiptap 에디터의 사용 방법에 대해 알아보았다. 라이브러리 자체에서 제공하는 사용 방법이 워낙 다양하고, 앞서 소개한 방식 외에 아예 대부분의 기능이 구현된 완전한 에디터 자체를 설치해서 사용할 수도 있다. 새롭게 에디터를 사용해야 하는 상황이 생긴다면, 권해보고 싶은 라이브러리인 것 같다!