[React] Nextjs에서 tui.editor 사용하기
2020 / 04 / 05
Next.js에 tui.editor를 사용하는 예제입니다: https://github.com/myeongjae-kim/tui.editor-with-nextjs
tui.editor 2.0이 릴리즈되었습니다.
![](https://user-images.githubusercontent.com/1215767/34356204-4c03be8a-ea7f-11e7-9aa9-0d84f9e912ec.gif#shadow)
대표적인 위지윅 에디터 draft-js와 quill 모두 써봤지만 Markdown 지원이 아쉬워서 그냥 기본적인 텍스트박스와 marked를 이용해 에디터를 구현해서 사용하고 있는데, tui.editor를 보니 정말 마음에 들었습니다. 블로그 에디터로 먼저 활용해본 뒤 괜찮으면 업무에도 적용해야지.
Next.js에서 import해서 사용하려고 하니 Server Side Rendering때문에 window객체를 못 찾는다는 에러가 발생합니다.
![tui-editor-window-is-not-defined.png](https://cdn.myeongjae.kim/blog/2020/04/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](https://cdn.myeongjae.kim/blog/2020/04/code-mirror-navigator-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;