
프로젝트에서 텍스트 에디터를 사용해야하기 때문에 몇가지 라이브러리를 알아보면서 시도했던 부분과 선택한 텍스트 에디터를 정리해보는 글입니다.
일단 텍스트 편집기를 처음 사용해보기도 하고 선택하는 기준으로 텍스트 에디터를 빠르게 적용 가능하고 가장 기본적인 기능과 편리하게 사용할 수 있는 점의 기준이였습니다. 그래서 고민했던 3가지 텍스트 에디터 라이브러리에 대하여 시도하여 적용하는 과정입니다.
시도
가장 먼저 toast ui를 선택했습니다. 선택한 이유는 에디터의 충분한 기능들이 있으며 마크다운 방식을 지원하고 현재 작성하고있는 velog 처럼 텍스트 에디터를 작성하면서 바로 preview를 볼 수 있기 때문에 선택했습니다.
하지만 Next14 버전에서 toast ui를 설치를 하면 아래와 같이 에러가 발생합니다.
ERESOLVE unable to resolve dependency tree의 이 에러는 의존성을 해결할 수 없을 경우 발생하는 에러입니다.
자세히 보면 현재 Next14 프로젝트에서 react 버전이 18인데 toast ui editor는 react 17버전을 요구합니다.
즉 react 버전이 맞지않아 발생하는 에러입니다.

의존성 충돌 문제를 해결하기위해 --legacy-peer-deps 옵션을 사용하면 npm이 패키지간의 의존성을 무시하고 설치가 가능합니다. 하지만 이 방법으로 의존성 충돌이 발생하는 패키지 에러를 무시하고 설치를 진행한다면 나중에 설치한 에디터의 특정 기능이 react 18버전의 환경에서 제대로 작동되지 않는 문제가 발생할 가능성 있고 현재 프로젝트의 react 버전을 toast ui editor로 인해 17버전으로 변경할 예정이 없었기 때문에 toast ui editor를 사용하지 않았습니다.
2번째로 react-quill를 선택한 이유는 기본적인 기능들과 커스텀이 가능하고 별도의 복잡한 설정없이 편집 기능을 바로 사용할 수 있어 선택했습니다.
그리고 텍스트 에디터 편집기를 검색해보면 react-quill 사용기가 많이 보였고 npm trend 참고하면 quill이 가장 오래전에 만들어져 다운로드 수도 많았습니다.
먼저 react-quill을 설치하고 next에서 적용하면 ReferenceError: document is not defined 이런 에러를 볼 수 있습니다. 에러의 원인으로 react-quill은 내부적으로 브라우저의 window 객체나 document 객체에 의존합니다. 하지만 next ssr 환경에서는 브라우저 환경이 아니므로 window와 document가 존재하지 않습니다. 그래서 react-quill을 ssr 환경에서 렌더링하려고 하면 위와 같은 에러가 발생하게됩니다.
해결 방법으로는 비동기적으로 컴포넌트를 로드하는 next/dynamic으로 해결할 수 있습니다.
import React from 'react';
import dynamic from 'next/dynamic';
import ReactQuill, { ReactQuillProps } from 'react-quill';
interface IForwardedQuillComponent extends ReactQuillProps {
forwardedRef: React.Ref<ReactQuill>;
}
const QuillNoSSR = dynamic(
async () => {
const { default: QuillComponent } = await import('react-quill');
const Quill = ({ forwardedRef, ...props }: IForwardedQuillComponent) => (
<QuillComponent ref={forwardedRef} {...props} />
);
return Quill;
},
{ loading: () => <div>...loading</div>, ssr: false },
);
export default QuillNoSSR;
next/dynamic을 사용하여 비동기적으로 컴포넌트를 로드하고 ssr: false 옵션을 설정해서 서버 환경에서는 해당 컴포넌트 랜더링을 비활성화 하기 때문에 관련 에러를 방지할 수 있습니다.
이제 설정을 마친 에디터 컴포넌트를 사용하려고 보면 콘솔창에 경고가 아래와 같이 나옵니다.

경고메세지를 보면 react-quill이 사용하는 DOMNodeInserted 이벤트가 더 이상 권장되지 않는 이벤트의 경고입니다. 현재 실제 기능 동작에는 영향을 주지않지만 추후에 브라우저 업데이트에서 해당 이벤트가 제거될 가능성이 있습니다. 그래서 크롬 및 최신 브라우저에서는 MutationObserver로 대신 사용하도록 권장해서 DOMNodeInserted 사용 시 위와 같은 경고가 발생합니다.
해결 하려고 찾아보니 react-quill-new의 라이브러리가 해당 이슈를 해결해준다고 나와있습니다.
npmjs에서 react-quill-new 라이브러리 설명을 참고하면 "react-quill 의 포크 로, QuillJS 종속성을 1.3.7에서 >=2.0.2로 업데이트하고 원래 관리자가 더 이상 활동하지 않으므로 종속성 업데이트와 문제를 최신 상태로 유지하려는 것"으로 명시되어있습니다.
결론은 react-quill이 아닌 최신의 react-quill-new의 라이브러리를 사용해야합니다.
그럼 이럴바엔 처음 텍스트 에디터를 고민할때 npm trends에서 stars도 많았고 업데이트도 꾸준히 잘되고있는 tiptap 에디터를 시도 해보고자 하는 이유로 react-quill를 바로 적용하지 않았습니다.
3번째 시도의 tiptap은 사용자가 원하는대로 정확하게 만들 수 있는 오픈 소스 헤드리스 텍스트 편집기 입니다. 커스텀이 가능하고 next와도 호환성이 좋다고 알려져 있어 선택을 했습니다.
그리고 2번째 마지막 부분에서 설명한것처럼 tiptap 에디터가 꾸준히 업데이트도 잘되며 1년간 다운로드 그래프 보면 고민했던 3개의 라이브러리중 상승세 입니다.
이런 이유임에도 처음부터 선택(시도)하지 않았던 이유는 헤드리스 편집기로써 커스텀의 자유도를 높여주는 대신에 모든 요소를 설정해 줘야 합니다. 그래서 위 2가지의 텍스트 편집기보다 적용할때 시간이 좀 더 걸릴것으로 생각해서 처음부터 적용하지 않았었습니다.
(결국에 tiptap으로 텍스트 편집기로 적용)

먼저 tiptap을 설치하고 기본적인 기능으로 스타터킷 확장을 설치해주고 필요시에 추가 확장을 따로 설치합니다.
그리고 tiptap은 헤드리스 텍스트 편집기여서 처음 설치하면 기본적인 툴바가 없고 textarea 같은 라인만 나옵니다. 그래서 필요한 기능들의 확장을 설치하고 tiptap extension을 참고하여 필요한 toolbar 확장을 추가해야합니다.
// toolbar 설정
<div className="flex items-center gap-3 border-b border-gray-500 p-2 max-sm:gap-1 max-sm:overflow-x-auto">
<div className="flex items-center gap-4 transition-all max-sm:gap-1">
<button
type="button"
className="toolbarBtn"
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
aria-label="제목 크기 1"
>
<Heading1 className="h-5 w-5" />
</button>
<button
type="button"
className="toolbarBtn"
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
aria-label="제목 크기 2"
>
<Heading2 className="h-5 w-5 cursor-pointer" />
</button>
<button
type="button"
className="toolbarBtn"
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
aria-label="제목 크기 3"
>
<Heading3 className="h-5 w-5 cursor-pointer" />
</button>
<div className="h-5 w-[1px] bg-gray-400" />
</div>
<div className="flex items-center gap-4 transition-all max-sm:gap-1">
<button
type="button"
className="toolbarBtn"
onClick={() => editor.chain().focus().toggleBold().run()}
aria-label="텍스트 진하게"
>
<Bold className="h-5 w-5 cursor-pointer" />
</button>
<button
type="button"
className="toolbarBtn"
onClick={() => editor.chain().focus().toggleItalic().run()}
aria-label="텍스트 기울기"
>
<Italic className="h-5 w-5 cursor-pointer" />
</button>
<button
type="button"
className="toolbarBtn"
onClick={() => editor.chain().focus().toggleUnderline().run()}
aria-label="텍스트 밑줄"
>
<Underline className="h-5 w-5 cursor-pointer" />
</button>
<div className="h-5 w-[1px] bg-gray-400" />
</div>
<div className="flex items-center gap-4 transition-all max-sm:gap-1">
<button
type="button"
className="toolbarBtn"
onClick={() => editor.chain().focus().toggleBulletList().run()}
aria-label="순서가 없는 리스트 정렬"
>
<List className="h-5 w-5 cursor-pointer" />
</button>
<button
type="button"
className="toolbarBtn"
onClick={() => editor.chain().focus().toggleOrderedList().run()}
aria-label="순서가 있는 리스트 정렬"
>
<ListOrdered className="h-5 w-5 cursor-pointer" />
</button>
<div className="h-5 w-[1px] bg-gray-400" />
</div>
<div className="flex gap-4 transition-all max-sm:gap-1">
<TiptabImage editor={editor} />
<TiptabLink editor={editor} />
</div>
</div>
위처럼 heading, bold, italic, underline, bulletList, orderlist, image, link 이렇게 가장 기본적인 기능들로 툴바를 구성했습니다.
그리고 placeholder, link, imageResize 확장을 추가로 설치해주고 아래와 같이 에디터를 구성했습니다.

// tiptap 에디터
const editor = useEditor({
editorProps: {
attributes: {
class: "prose m-4 focus:outline-none tiptap",
},
},
extensions: [
StarterKit,
Underline,
ImageResize,
Placeholder.configure({
placeholder:
"모임에 대한 설명을 작성해주세요.\n※ 이미지 선택 후 에디터에 표시된 이미지를 클릭하여 사이즈를 조정할 수 있습니다.",
}),
Link.extend({
inclusive: false,
}).configure({
defaultProtocol: "https",
protocols: ["http", "https"],
openOnClick: true,
autolink: false,
}),
],
content: field.value,
onUpdate: ({ editor: updatedEditor }) => {
field.onChange(updatedEditor.getHTML());
},
immediatelyRender: false,
});
위 처럼 확장과 툴바의 설정을 마치고 다른 기능들은 작동이 되지만 heading의 기능이 작동하지 않았습니다.
찾아보니 현재 스타일링을 tailwind로 사용하고 있는데 공식문서의 tiptap styling을 읽어보면 tailwind는 typography의 플러그인 설치에 대해 나와있습니다. 해당 플러그인을 설치하고 tailwind config에서 플러그인을 추가하면 heading이 잘 작동합니다.
왜인지 이유를 찾아보니 tiptap의 heading 확장은 태그를 생성하는데 tailwind는 기본적으로 h1, h2, h3 등의 태그에 스타일을 적용하지 않습니다. 그래서 tailwind 에서는 기본 스타일이 없어 적용이 안되는 경우입니다.
typography 플러그인 설치하면 prose 클래스를 추가해 자동으로 적절한 스타일을 적용해서 잘 적용이 됩니다.

경고 메세지에서 해결방법의 힌트를 볼 수 있는데 immediatelyRender: false 옵션을 설정하면 hydration이 완료된 후에 에디터가 렌더링 하기때문에 서버와 클라이언트 간의 불일치하는 문제를 해결할 수 있습니다.
마무리

시도 3번째 만에 최종적으로 tiptap 텍스트 편집기를 선택했습니다.
팀 프로젝트로써 시간적인 면이 있다 보니 빠르게 적용하여 테스트해보는 부분이 우선이었습니다. 그래서 개인적으로 적용하는 부분에서 tiptap 적용이 오래 걸릴 수도 있다는 생각이 있어 우선순위가 후순위였고 고민했던 다른 텍스트 편집기를 먼저 시도를 했었습니다. 물론 tiptap을 적용해보면서 시간이 걸렸지만 보기에도 문서가 편리하게 정리되어 있어 참고하여 적용하는데 정말 많은 도움이 되었고 next에서 텍스트 편집기 라이브러리 선택하는 좋은 시도 과정이었다고 생각합니다.