Next.js에 tui.editor를 사용하는 예제입니다: https://github.com/myeongjae-kim/tui.editor-with-nextjs


tui.editor 2.0이 릴리즈되었습니다.

대표적인 위지윅 에디터 draft-jsquill 모두 써봤지만 Markdown 지원이 아쉬워서 그냥 기본적인 텍스트박스와 marked를 이용해 에디터를 구현해서 사용하고 있는데, tui.editor를 보니 정말 마음에 들었습니다. 블로그 에디터로 먼저 활용해본 뒤 괜찮으면 업무에도 적용해야지.

Next.js에서 import해서 사용하려고 하니 Server Side Rendering때문에 window객체를 못 찾는다는 에러가 발생합니다.

tui-editor-window-is-not-defined.png

tui.editor측에서는 ssr을 지원할 계획이 아직 없다고 합니다. 하긴 위지윅 에디터가 SSR을 지원해서 얻는 이득이 뭐가 있을까요...

보통 아래처럼 typeof window !== "undefined"로 가드를 해주면 SSR 문제가 사라지는데, 이번에는 이 방법으로는 해결되지 않았습니다. 나중에 알고보니 이번 에러는 모듈을 사용할 때가 아니라 import할 때 발생하기 때문에 node상에서 모듈을 import하지 않도록 근본적으로 막아야 했습니다.

// Still window is not defined...
import * as React from 'react';
import { Editor } from '@toast-ui/react-editor';

const WysiwygEditor: React.FC = () => {
  return <>
    {typeof window !== "undefined" && <Editor
      initialValue="hello react editor world!"
      previewStyle="vertical"
      height="600px"
      initialEditType="markdown"
      useCommandShortcut={true}
    />}
  </>;
}

export default WysiwygEditor;

node_modules/@toast-ui/editor/dist/toastui-editor.js:16:4를 확인해보니 window객체가 있어서, window대신 this를 사용하면 문제가 해결될지도..? webpack.config.js에 옵션 하나만 추가하면 window대신 this를 사용할 수 있습니다.

module.exports = {
  output: {
    globalObject: 'this'
  }
}

tui.editor를 clone해서 webpack.config.js에 위 설정을 추가하고 빌드해보니 window is not defined 에러는 없어졌지만 이번에는 CodeMirror에서 navigator is not defined 에러가 발생합니다.

tui-editor-window-is-not-defined.png

CodeMirror도 마찬가지로 SSR을 지원할 계획이 없고, Next.js에서 CodeMirror를 사용하기 위해 SSR을 disable하는 코드를 찾았습니다.

import dynamic from 'next/dynamic';
const CodeMirror = dynamic(() => import('react-codemirror'), { ssr: false });

이미 Next.js 문서에 With no SSR이라는 항목으로 예제가 있었습니다. 라이브러리 최신 기능을 재깍재깍 학습하면 이고생 안했을텐데요 😭

tui-editor에도 동일하게 적용해보니 더이상 에러가 발생하지 않습니다.

import dynamic from 'next/dynamic';
import * as React from 'react';
import { EditorProps } from '@toast-ui/react-editor';

const Editor = dynamic<EditorProps>(() => import('@toast-ui/react-editor').then(m => m.Editor), { ssr: false });

const WysiwygEditor: React.FC = () => {
  return <Editor
    initialValue="hello react editor world!"
    previewStyle="vertical"
    height="600px"
    initialEditType="markdown"
    useCommandShortcut={true}
  />;
}

export default WysiwygEditor;

잘 작동하는 줄 알았으나... Next.js의 dynamic()때문에 ref를 제대로 사용할 수 없어서 정작 에디터의 내용을 가져올 수 없었습니다. React.fowardRef()를 활용해서 에디터의 ref에 접근하는 방법을 사용해 해결했습니다(https://github.com/zeit/next.js/issues/4957#issuecomment-413841689)

import React from "react";
import { Editor, EditorProps } from "@toast-ui/react-editor";

export interface TuiEditorWithForwardedProps extends EditorProps {
   forwardedRef?: React.MutableRefObject<Editor>;
}

export default (props: TuiEditorWithForwardedProps) => (
  <Editor {...props} ref={props.forwardedRef} />
); 
import dynamic from 'next/dynamic';
import * as React from 'react';
import { Editor as EditorType, EditorProps } from '@toast-ui/react-editor';
import { TuiEditorWithForwardedProps } from './TuiEditorWrapper';

interface EditorPropsWithHandlers extends EditorProps {
  onChange?(value: string): void;
}

const Editor = dynamic<TuiEditorWithForwardedProps>(() => import("./TuiEditorWrapper"), { ssr: false });
const EditorWithForwardedRef = React.forwardRef<EditorType | undefined, EditorPropsWithHandlers>((props, ref) => (
  <Editor {...props} forwardedRef={ref as React.MutableRefObject<EditorType>} />
));

interface Props extends EditorProps {
  onChange(value: string): void;

  valueType?: "markdown" | "html";
}

const WysiwygEditor: React.FC<Props> = (props) => {
  const { initialValue, previewStyle, height, initialEditType, useCommandShortcut } = props;

  const editorRef = React.useRef<EditorType>();
  const handleChange = React.useCallback(() => {
    if (!editorRef.current) {
      return;
    }

    const instance = editorRef.current.getInstance();
    const valueType = props.valueType || "markdown";

    props.onChange(valueType === "markdown" ? instance.getMarkdown() : instance.getHtml());
  }, [props, editorRef]);

  return <div>
    <EditorWithForwardedRef
      {...props}
      initialValue={initialValue || "hello react editor world!"}
      previewStyle={previewStyle || "vertical"}
      height={height || "600px"}
      initialEditType={initialEditType || "markdown"}
      useCommandShortcut={useCommandShortcut || true}
      ref={editorRef}
      onChange={handleChange}
    />
  </div>;
};

export default WysiwygEditor;