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