안녕하세요

React-Quill로 게시물 편집, DB에 저장하기 본문

Next13 블로그 프로젝트

React-Quill로 게시물 편집, DB에 저장하기

sakuraop 2023. 9. 18. 01:25

목차

1. router 만들기
2. 라우터에 들어갈 컴포넌트기 만들기
3. quill 만들기
4. server에 quill에 작성한 데이터 전송 (request)
5. DB에 저장

6. 저장 결과

7. react-quiil이 content를 저장하는 원리


1. router를 만들어줍니다.

  • /manage/newpost 경로에 라우터를 만들어줍니다.
import Editor from "@/containers/Editor/Editor";

const NewPostRouter = async () => {
  return (
    <div>
      <Editor />
    </div>
  );
};

export default NewPostRouter;
  • Router는 라우팅 역할만 맡도록 합니다.

2. /src/containers/Editor/Editor.tsx 컴포넌트를 만들어 줍니다.

const Editor = () => {
  const [contents, setContents] = useState("");

  const quillProps = {
    contents,
    setContents,
  };

  return (
    <div className={styles.container}>
      <div className={styles.quillContainer}>
        <Quill {...quillProps} />
      </div>
    </div>
  );
};

export default Editor;
  • 라우터에 전달할 에디터를 컴포넌트로 만듭니다.

3. quill을 Editor 안에 만들어줍니다.

https://www.npmjs.com/package/react-quill#quick-start

- react-quill npm 공식 문서

 

가장 기본적인 형태는 다음과 같습니다.

function MyComponent() {
  const [value, setValue] = useState('');

  return <ReactQuill theme="snow" value={value} onChange={setValue} />;
}

 

https://quilljs.com/docs/modules/toolbar/

- react-quill toolbar 커스텀 방법

이 커스텀 옵션 사진과 아래의 코드로 커스텀 방법을 쉽게 설명해보겠습니다.

  const modules = {
    toolbar: {
      container: [
        ["underline", "strike", "blockquote"], // 글자 효과
        [{ size: ["small", false, "large", "huge"] }], // 글자 크기
        [{ color: [] }, { background: [] }], // 글자 색상, 글자 배경
        ["image", "video"],
      ],
    },
  };
  • 컨테이너의 역할은 기능을 구분하는 경계입니다.

[ [기능1], [기능2], [기능3], [기능4] ] 
이렇게 구분된 형태로 이해할 수 있습니다.

 

        [{ color: [] }, { background: [] }], // 글자 색상, 글자 배경
  • 작은 배열 안에 들어 있는 {color: []}, {background: []} 와 같은 것들이 속성을 결정합니다.

 

이렇게 만들어 준 modules 객체를 <ReactQuill modules={moduels} /> 와 같이 설정해주면 적용이 됩니다.

제가 만든 코드 예시)

"use client";

import ReactQuill from "react-quill";
import "react-quill/dist/quill.snow.css";
import styles from "./Quill.module.scss";

interface quillProps {
  contents: string;
  setContents: React.Dispatch<React.SetStateAction<string>>;
}

const Quill = ({ contents, setContents }: quillProps) => {
  const modules = {
    toolbar: {
      container: [
        ["underline", "strike", "blockquote"], // 글자 효과
        [{ size: ["small", false, "large", "huge"] }], // 글자 크기
        [{ color: [] }, { background: [] }], // 글자 색상, 글자 배경
        ["image", "video"],
      ],
    },
  };

  return (
    <ReactQuill
      className={styles.quill} // 없어도 됩니다.
      value={contents} // value에는 input에 들어갈 state를 넣어줍시다.
      onChange={setContents} // onChage에는 input을 바꿔줄 setState를 넣어줍시다.
      modules={modules} 
      theme="snow" // 테마도 커스텀이 가능합니다. snow가 기본값입니다. 없어도 됩니다.
      placeholder="내용을 입력해주세요." // 없어도 됩니다.
    />
  );
};

export default Quill;

4. server에 데이터를 보내봅시다. (request)

  • 이렇게 만들어 볼겁니다.

예시 코드) (placeholder와 import는 알아서 하시면 됩니다.)

"use client";

import React, { useState } from "react";
import Quill from "./Quill/Quill";
import styles from "./Editor.module.scss";
import axios from "axios";
import { useRouter } from "next/navigation";

const Editor = () => {
  // 작성 완료되면 게시물로 redirect 할겁니다.
  const router = useRouter(); 

  const [title, setTitle] = useState("");
  const [subtitles, setSubtitles] = useState("");
  const [languages, setLanguages] = useState("");
  const [contents, setContents] = useState("");

  // quill에 전달할 state props 
  const quillProps = { 
    contents,
    setContents,
  };

  // 작성하기 버튼 클릭하면,
  const handleClickWriteButton = async (e: any) => {
    e.preventDefault();
    // api 요청을 보내고
    const result = await axios.post("/api/manage/newpost", {title, subtitles, languages, contents});

    // 게시물 작성이 완료되면 해당 게시물 주소로 redirect 합니다.
    router.push(`/post/${result.data.id}`);
  };

  return (
    <div>
      <div>
        {/* 제목 부제목 언어 input 굳이 없어도 됩니다. */}
        <input type="text" value={title} onChange={(e) => setTitle(e.target.value)}/>
        <input type="text" value={subtitles} onChange={(e) => setSubtitles(e.target.value)}/>
        <input type="text" placeholder="languages" value={languages} onChange={(e) => setLanguages(e.target.value)}/>
      </div>
      <div>
        {/* state와 setState 전달합니다. */}
        <Quill {...quillProps} />
      </div>
      <div>
        <button onClick={(e) => handleClickWriteButton(e)}>작성하기</button>
        <button>취소하기</button>
      </div>
    </div>
  );
};

export default Editor;

5. DB에 저장해봅시다.

  • 아래와 같이 api요청 경로로 '/api/manage/newpost' 폴더와 파일을 생성합니다. 

수행할 작업은 다음과 같습니다.

  1. POST 요청인지 확인
  2. DB연결
  3. 게시물 작성자 정보 확인
  4. 게시물 번호 추가 (없어도 됨)
  5. DB에 게시물 정보 저장
  6. 게시물 번호를 포함하여 200 응답 => redirect할 주소로 이용됩니다.

기타 자잘한 설명은 주석에 써 놓았습니다.

코드 예시)

import { connectDB } from "@/utils/db/db";
import { getToken } from "next-auth/jwt";

const handler = async (req: any, res: any) => {
  if (req.method == "POST") {
    // DB와 Collection 연결
    const db = (await connectDB).db("blog");
    const postCollection = await db.collection("posts");
    const countersCollection = await db.collection("counters");

    // 게시물 작성자 정보
    const token = await getToken({ req });

    // 게시물 번호 정보 업데이트하기
    const currentCounter = await countersCollection.findOne({ model: "posts" });
    await countersCollection.findOneAndUpdate(
      { model: "posts" },
      { $inc: { count: 1 } },
      { upsert: true }
    );

    const { title, subtitles, languages, contents, src } = req.body; // 게시물 내용
    const id = currentCounter ? currentCounter.count + 1 : 1; // 게시물 번호
    const email = token?.email; // 작성자 email
    const author = token?.name; // 작성자 닉네임
    const date = new Date(); // 게시물 작성 시점
    const commentCount = 0; // 댓글
    const likes = 0; // 좋아요

    const saveData = {
      id,
      title,
      subtitles: subtitles.split(" "),
      languages: languages.split(" "),
      contents,
      email,
      src: src || "https://cdn.pixabay.com/photo/2023/09/03/11/13/mountains-8230502_1280.jpg",
      author,
      date,
      commentCount,
      likes,
    };

    const result = await postCollection.insertOne({ ...saveData }); // DB에 저장한 결과

    return res.status(200).json({ id }); // 응답에 게시물 id를 포함하여 redirect할 수 있도록 합니다.
  }
};

export default handler;

6. 결과

게시물 목록

게시물 상세 페이지


7. react-quiil이 content를 저장하는 원리

 

const Quill = ({ contents, setContents }: quillProps) => {
  useEffect(() => {
    console.log(contents);
  }, [contents]);

작성하면서 출력을 해보면 됩니다.

 

작성된 글은 태그로 감싸지고,

그 안에 드래그된 텍스트나 커서 위치를 포함하는 태그에 style이 추가되는 방식입니다.