진행중인 프로젝트에서 맡은 게시글 작성 페이지 figma 스크린샷이다. 전에 했던 프로젝트에서 게시글 작성 페이지를 구현했지만 기능적으로도 UI도 만족하지 못했다. 그래서 이번에 작성페이지 개발을 희망한다고 팀원들에게 말하고 다시 도전! 피그마 스크린샷 toolbar를보면 작성 에디터를 어떤 것을 쓸지 정하지 않아 일단은 벨로그에 있는 toolbar를 캡쳐해서 넣어놨다.
어떤 라이브러리를 해야할 지 고민 하던 중 toast ui 라는 라이브러리가 UI가 깔끔한 것 같아서 사용하려고 했다.
안타깝게도.. 현재 우리 프로젝트는 리액트 18 버젼을 사용중인데 toast ui는 리액트 17버젼 까지만 지원이 된다고 한다.😭
구글 검색을 통해 사람들의 평가가 좋았던 Quill edit로 설치해서 사용해보기로 했다.
Quill edit를 사용하려고 마음 먹은 이유는
모듈식 아키텍처와 표현식 API: Quill은 모듈식 아키텍처(필요한 기능만 선택가능, 유지보수 쉬움) 와 표현식 API(복잡한 DOM 조작을 피함)를 제공한다.
( 다른 편집기를 사용안해봐서 다른 편집기에서의 어떤 이슈가 있는 지는 아직 알지 못한다.)
다른 대안들과의 비교: toast ui가 일단 현재 진행중인 프로젝트 버전에서는 사용이 불가능 했고 다른 편집기로는 대표적으로 Facebook이 지원하는 Draft.js가 있다. 하지만 Draft.js는 자체 버그와 CSS 충돌 이슈가 있다고 한다.
공식사이트와 npm 지원: 공식사이트에 설명이 친절하게 나와있고, 사용자도 많아 기본적인 정보 얻는데 불편함이 없을 것 같았다.
현재 진행 중인 Next.js 프로젝트에서 Quill Edit를 통합하려고 했는데 문제가 발생했다 . Quill의 공식 문서와 npm 설명을 따라 설치하고 설정했음에도 불구하고 에러가 발생하여 제대로 작동하지 않는다..
Quill은 ssr 지원이 되지 않기 때문에 단순히
import ReactQuill from react-quill
와 같이 static import를 하면 에러가 뜬다.
간단하게 설명하면 서버에서 사용자에게 보여줄 페이지를 모두 미리 구성한 뒤 페이지를 렌더링을 하는 방식을 의미한다.
react-quill을 import할 때에는 바로 상단에 import하지 않고, next의 dynamic을 사용하여 import 해야한다. 이렇게 처리해주지 않으면 'document is not defined'에러가 나오는데, 이는 react-quill에서는 ssr이 지원되지 않기 때문이다. document가 정의되기 전에 react-quill이 먼저 로드 되어 아직 정의되지 않은 document를 조작하려고 하면서 발생하는 에러가 발생한다.
JavaScript 라이브러리나 컴포넌트는 브라우저에서만 작동하는 코드(예: document나 window와 같은 브라우저 전용 객체에 의존하는 코드)를 포함하고 있어서 서버 사이드 렌더링 환경에서 제대로 작동하지 않을 수 있다고 한다.. React-Quill도 이런 라이브러리 중 하나라고 한다.
Next.js는 JavaScript 용 ES2020 dynamic import (git hub link) 이것은 모듈을 동적으로 import할 수 있도록 지원해준다고 한다.
자세한 내용은 (https://dev.to/nialljoemaher/dynamic-importing-code-splitting-es2020-3dm3)
해결방법은
import dynamic from 'next/dynamic'
dynamic-import를 사용하여 Quill Editor를 서버 측에 모듈을 포함하지 않도록 하고 브라우저에서만 작동하도록 하는 방법을 사용해서 해결할 수 있었다.
아래와 같이 작성하여 만든 quill 컴포넌트를 만들었고, 원하는 페이지에서 dynamic import로 edit를 동적으로 import할 수 있도록한다.
import React from "react";
import "react-quill/dist/quill.snow.css";
import { QuillWrapper } from "./index.styled";
import ReactQuill, { Quill } from "react-quill";
import ImageResize from "quill-image-resize";
Quill.register("modules/ImageResize", ImageResize);
interface QuillEditorProps {
value: string;
onChange: (content: string) => void;
}
//실제 동작할 기능 설정
export const formats = [
"header",
"bold",
"italic",
"underline",
"strike",
"align",
"list",
"ordered",
"bullet",
"background",
"color",
"link",
"image",
];
const QuillEditor: React.FC<QuillEditorProps> = ({ value, onChange }) => {
const modules = {
// 아래 기능이 toolbar에 나타 남
toolbar: [
[{ header: [1, 2, 3, false] }],
[{ align: [] }],
["bold", "italic", "underline", "strike"],
[{ list: "ordered" }, { list: "bullet" }],
[{ color: [] }, { background: [] }],
["link", "image"],
],
ImageResize: {
parchment: Quill.import("parchment"),
},
};
return (
<QuillWrapper
value={value}
onChange={(content, delta, source, editor) => {
//실시간 미리보기를 위한 함수
onChange(editor.getHTML());
}}
modules={modules}
formats={formats}
/>
);
};
export default QuillEditor;
"use client";
import React from "react";
import * as PW from "./page.styled";
import dynamic from "next/dynamic";
const QuillEditor = dynamic(() => import("@/components/QuillEditor"), { ssr: false });
export default function PostWrite() {
const [value, setValue] = React.useState("");
const [tags, setTags] = React.useState<string[]>([]);
const [tagInput, setTagInput] = React.useState<string>("");
//태그 입력 함수
const handleTagInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setTagInput(event.target.value);
};
// 태그 입력 시 엔터 가능하게 하는 함수 , 태그는 5개까지만 가능
const handleTagInputKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter" && tagInput.trim() !== "" && tags.length < 5) {
setTags([...tags, tagInput]);
setTagInput("");
} else if (tags.length >= 5) {
alert("태그는 최대 5개까지만 추가할 수 있습니다.");
}
};
// 태그 삭제 버튼 함수
const handleTagDelete = (index: number) => {
const newTags = [...tags];
newTags.splice(index, 1);
setTags(newTags);
};
// async const createPost = () => {
// try {
// const response = await axios.get(``);
// if (response.status === 200) {
// setMembers(response.data.data);
// } else {
// console.error('멤버 정보 가져오기 에러:', response.status);
// }
// } catch (error) {
// console.error('멤버 정보 가져오기 에러:', error);
// }
return (
<PW.Wrapper>
<PW.LeftDisplay>
<PW.WriteTitleText>게시글 작성</PW.WriteTitleText>
<PW.WriteSection>
<PW.TitleAndLocation>
<PW.TitleInput type="text" placeholder="제목을 입력해주세요" />
<PW.LocationWrapper>
<PW.LocationIcon />
<PW.LocationInput type="text" placeholder="위치" />
</PW.LocationWrapper>
</PW.TitleAndLocation>
<PW.TagsAndRocket>
<PW.TagsInputWrapper>
<PW.TagIcon />
<input
type="text"
placeholder="태그"
value={tagInput}
onChange={handleTagInputChange}
onKeyPress={handleTagInputKeyPress}
/>
</PW.TagsInputWrapper>
<PW.RocketInputWrapper>
<PW.RocketIcon />
<input type="text" placeholder="우주선" />
</PW.RocketInputWrapper>
</PW.TagsAndRocket>
<PW.TagsDisplay>
{tags.map((tag, index) => (
<PW.TagWrapper key={index}>
<PW.Tags>
{tag}
<PW.DeleteTagButton onClick={() => handleTagDelete(index)}>X</PW.DeleteTagButton>
</PW.Tags>
</PW.TagWrapper>
))}
</PW.TagsDisplay>
<QuillEditor value={value} onChange={setValue} />
<PW.ButtonGroup>
<PW.BackBtn>뒤로</PW.BackBtn>
<PW.CompletedBtn>작성 완료</PW.CompletedBtn>
</PW.ButtonGroup>
</PW.WriteSection>
</PW.LeftDisplay>
<PW.PreviewSection dangerouslySetInnerHTML={{ __html: value }} aria-readonly />
</PW.Wrapper>
);
}
dangerouslySetInnerHTML: React에서 원시 HTML을 직접 삽입하기 위해 사용하는 속성
{ __html: value }는 value 상태 값을 HTML로 직접 삽입하여 QuillEditor에서 작성된 내용이 실시간으로 미리보기 섹션에 표시되게 했다.
__html:value 에서 value는 'useState'에서 정의된 상태의 value를 참조한다.
미리보기의 간단한 구조는 Quill Editor 컴포넌트에서 이 상태변수를 prop으로 전달하여 Editor내에 사용자의 입력에 따라 setValue 함수가 호출되어 value 값을 업데이트한다.
그리고 이 value를 __html:value로 반영시켜 미리보기 섹션에서 반영된다
필요한 toolbar를 모두 가져왔고, 미리보기에 value를 잘 전달하고 있다.
다음 글에서 미리보기와, 이미지 크기에 대한 이슈를 작성해보려 한다.🔥