nextjs 프로젝트에서 @toast-ui/react-editor
라는 에디터를 사용하고 있습니다. 하지만 페이지에서 에디터 컴포넌트를 불러오면 다음과 같이 에러를 뱉어내고 있어요.
왜 이런 현상이 발생할까요?
아래는 에러 로그와 에디터 컴포넌트,컴포넌트를 불러오는 페이지 컴포넌트입니다.
에러 발생!
⨯ ReferenceError: Element is not defined
at __webpack_require__ (/Users/jjalseu/IdeaProjects/FE_jjalseu_playground/.next/server/webpack-runtime.js:33:42)
at __webpack_require__ (/Users/jjalseu/IdeaProjects/FE_jjalseu_playground/.next/server/webpack-runtime.js:33:42)
at eval (./app/_components/editor/textEditor.tsx:9:80)
at (ssr)/./app/_components/editor/textEditor.tsx (/Users/jjalseu/IdeaProjects/FE_jjalseu_playground/.next/server/app/(page)/quiz/register/page.js:183:1)
at __webpack_require__ (/Users/jjalseu/IdeaProjects/FE_jjalseu_playground/.next/server/webpack-runtime.js:33:42)
at eval (quiz/register/components/client/quizRegisterForm.tsx:14:91)
at (ssr)/./app/(page)/quiz/register/components/client/quizRegisterForm.tsx (/Users/jjalseu/IdeaProjects/FE_jjalseu_playground/.next/server/app/(page)/quiz/register/page.js:161:1)
at Object.__webpack_require__ [as require] (/Users/jjalseu/IdeaProjects/FE_jjalseu_playground/.next/server/webpack-runtime.js:33:42)
digest: "3172729004"
GET /quiz/register 500 in 52ms
에디터 코드
"use client"
import React from 'react';
import {Editor} from "@toast-ui/react-editor";
import '@toast-ui/editor/dist/toastui-editor.css';
// code-syntax-highlight
import 'prismjs/themes/prism.css';
import '@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css';
import codeSyntaxHighlight from '@toast-ui/editor-plugin-code-syntax-highlight';
import Prism from 'prismjs'
const TextEditor = ({label}:{label:string}) => {
return (
<div>
<span className={"text-title3Normal"}>{label}</span>
<Editor
previewStyle="vertical"
initialValue={"editor"}
useCommandShortcut={true}
plugins={[[codeSyntaxHighlight,{ highlighter: Prism }]]}
/>
</div>
);
};
export default TextEditor;
에디터를 불러오는 컴포넌트
"use client"
import React, {useState} from 'react';
import TextInput from "@/app/_components/input/textInput";
import Select from "@/app/_components/select/select";
import {
DUPLICATE_OPTIONS,
FIELD_OPTIONS,
LANGUAGE_OPTIONS,
LEVEL_OPTIONS,
TYPE_OPTIONS
} from "@/app/(page)/quiz/constant";
import PrimaryButton from "@/app/_components/button/primaryButton";
import MultipleChoiceContents from "@/app/(page)/quiz/register/components/client/multipleChoiceContents";
import TextEditor from "@/app/_components/editor/textEditor";
interface QuizForm{
quizTitle:string,
quizContent:string,
answer:string,
hint:string,
explanation:string,
type:"MULTIPLE_CHOICE"|"SUBJECTIVE",
field:string,
lang:string,
level:number,
isMultiple:boolean,// 객관식일 경우 중복 선택 여부
time:number,
}
const QuizRegisterForm = () => {
const initialQuizForm:QuizForm={
quizTitle:"",
quizContent:"",
answer:"",
hint:"",
explanation:"",
type:"MULTIPLE_CHOICE",
field:"",
lang:"",
level:1,
isMultiple:false,
time:0
}
const [quizForm,setQuizForm]=useState<QuizForm>(initialQuizForm);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
}
return (
<form
onSubmit={handleSubmit}
className={"flex flex-col gap-4 mt-[12px] items-center"}
>
<TextInput
label={"퀴즈 제목"}
className={"w-full"}
placeholder={"퀴즈 제목을 입력해주세요."}
/>
{/*퀴즈 내용 (텍스트 에디터)*/}
<TextEditor
label={"퀴즈 내용"}
/>
{/*힌트 (텍스트 에디터)*/}
{/*해설 (텍스트 에디터)*/}
<Select options={FIELD_OPTIONS} label={"분야"}/>
<Select options={LANGUAGE_OPTIONS} label={"언어"}/>
<Select options={LEVEL_OPTIONS} label={"문제 난이도"}/>
{/*문제 타입(객관식 or 주관식)*/}
<Select
options={TYPE_OPTIONS}
label={"문제타입"}
handleOptionChange={(value)=>setQuizForm({...quizForm,type:value as "MULTIPLE_CHOICE"|"SUBJECTIVE"})}
/>
{/*객관식일 경우,나타날 필드(중복 선택 여부)*/}
{quizForm.type==="MULTIPLE_CHOICE"&&<Select options={DUPLICATE_OPTIONS} label={"객관식 - 중복 선택 여부"}/>}
{/*객관식일 경우,나타날 필드(객관식 선택지 리스트)*/}
{quizForm.type==="MULTIPLE_CHOICE"&&<MultipleChoiceContents/>}
{/*문제풀이 소요시간*/}
<TextInput
type={"number"}
label={"문제풀이 소요시간(초)"}
className={"w-full"}
placeholder={"문제풀이 소요시간을 입력해주세요."}
/>
{/*문제*/}
<PrimaryButton
text={"퀴즈 등록"}
color={"primary"}
type={"submit"}
className={"!w-full !h-[40px]"}/>
</form>
);
};
export default QuizRegisterForm;
결론부터 말하자면 toast UI Editor라는 라이브러리는 SSR을 지원하지 않는다고 합니다.
Next.js는 기본적으로 서버 사이드에서 각 페이지를 사전 렌더링(pre-rendering)합니다. 즉, 모든 페이지를 서버에서 미리 렌더링하는데, 위에서 언급한 에디터 라이브러리는 이 사전 렌더링이 적용되지 않는다는 것입니다.
그렇다면 왜 이 라이브러리는 사전 렌더링이 적용되지 않는 걸까요?
Next.js에는 'dynamic function'이라는 기능이 있습니다. 이에 대한 힌트로 Next.js 공식 문서에서는 다음과 같이 설명하고 있습니다.
To dynamically load a component on the client side, you can use the ssr
option to disable server-rendering. This is useful if an external dependency or component relies on browser APIs like window
.
import dynamic from 'next/dynamic'
const DynamicHeader = dynamic(() => import('../components/header'),
{ ssr: false,})
window와 같은 브라우저 API에 의존하는 외부 디펜던시나 컴포넌트에 유용하다고 합니다. 그 말인 즉슨, 브라우저 API에 의존하고 있는 라이브러리 및 컴포넌트는 SSR이 되지 않는다는거죠!
서버사이드에서는 당연히 서버환경이니 브라우저 API(ex. window.localStorage)가 존재하지 않습니다. 그렇기 때문에 서버에서는 브라우저 API를 참조하고 있는 컴포넌트를 미리 렌더링하려고 하면 에러가 발생하는거죠.
위에서 잠깐 보셨듯이 nextjs는 이를 위해 dynamic function이라는 기능을 지원하고 있어요.
To dynamically load a component on the client side, you can use the ssr
option to disable server-rendering. This is useful if an external dependency or component relies on browser APIs like window
.
dynamic function은 서버사이드에서 프리렌더링하지 않도록 설정하게 하고 브라우저단에서 해당 컴포넌트에 해당하는 스크립트를 불러올 수 있도록 합니다. 즉, dynamic function이 사용된 컴포넌트는 브라우저단에서 해당 컴포넌트가 불러와지는 페이지에서 관련된 스크립트가 불러와지며 호출이 되니 에러가 날 일이 없죠!
즉, 클라이언트 사이드에서 적정 시점에 렌더링할 수 있게 해주는거예요.
그래서 저는 toast-ui 에디터 컴포넌트를 불러올 때, dynamic function을 사용하여 다음과 같이 변경해줬어요.
이전 에디터 컴포넌트
"use client"
import React from 'react';
import {Editor} from "@toast-ui/react-editor";
import '@toast-ui/editor/dist/toastui-editor.css';
// code-syntax-highlight
import 'prismjs/themes/prism.css';
import '@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css';
import codeSyntaxHighlight from '@toast-ui/editor-plugin-code-syntax-highlight';
import Prism from 'prismjs'
const TextEditor = ({label}:{label:string}) => {
return (
<div>
<span className={"text-title3Normal"}>{label}</span>
<Editor
previewStyle="vertical"
initialValue={"editor"}
useCommandShortcut={true}
plugins={[[codeSyntaxHighlight,{ highlighter: Prism }]]}
/>
</div>
);
};
export default TextEditor;
dynamic function을 적용한 toast-ui 에디터 컴포넌트
import dynamic from "next/dynamic";
import {EditorProps} from "@toast-ui/react-editor";
// TextEditor가 넘겨받는 props에요
import {TextEditorProps} from "@/app/_components/editor/textEditor";
const TextEditorNoSSR = dynamic(() => import("@/app/_components/editor/textEditor"), {
ssr: false,
loading: () => <p>텍스트 에디터 불러오는 중..</p>,
});
function TextEditorWrapper(props: EditorProps & TextEditorProps) {
return (
<TextEditorNoSSR
{...props}
/>
);
}
export default TextEditorWrapper;
이처럼 브라우저 API에 의존하고 있는 외부 라이브러리들 ,또 브라우저 API에 의존하고 있는 컴포넌트들은 위와 같이 dynamic function을 활용하여 브라우저단에서 불러와 렌더링할 수 있게 해주면 됩니다!