CKeditor5_이미지업로드 구현

해달·2021년 10월 10일
1

Project_01

목록 보기
1/3
post-thumbnail

CKeditor5 이미지 업로드


디렉토리 구조

├── README.md
├── client
│   ├── README.md
│   ├── package-lock.json
│   ├── package.json
│   ├── public
│   └── src
│       ├── App.css
│       ├── App.js
│       ├── components
│       │   ├── Editor
│       │   │   ├── Api.js
│       │   │   └── Editor.js

프로젝트에 적용할 에디터를 찾아보면서 CKeditor5를 사용하기로 결정 한 후에
구글에 엄청난 검색을 하면서 자료를 찾아가며 구현하려고 하였으나,

기존방식인 classic 방식을 설치 한 뒤에 이미지업로드 부분만 적용할 수 있는 방식으로 구현한 사람이 거의 없었다.
대부분 클래스형 ImageAdapter + 공식홈페이지에서 커스텀
or 커스텀 빌드
or eject
or CRCAO 로 구현이 되어있어서 코드를 막상 적용했어도 어떻게 쓸지가 너무 어려웠다

CKeditor 공식 홈페이지에도 설명은 매우 친절하게 되어 있으나
class형태의 컴포넌트로 작성되어 있어서 연결하는데 약간 어려움이 있을거 같았고
서버와 연동하는거에서 많이 헤매어서 거의 3-4일 동안
검색하고 코드 테스트해보고 시간을 무지하게 쏟았다.

그러다가 외국 블로그에 함수형 컴포넌트로 ImageAdapter를 구현해 놓은 코드가 있어서 (+서버) 정말 감사하게도 코드를 보면서 프로젝트에 적용을 시켰다
(도와주신 팀장님 감사해요)


  1. npm install --save @ckeditor/ckeditor5-build-classic
  2. 서버와 연동되도록 코드 구현
  3. package.json 확인하면서 npm으로 필요한 dependencies 설치

(제일 하기 싫었던 커스텀을 안해도 돼서 너무 좋았다 ㅠㅠ)


[최종이미지]


코드구현

Api.js

const express = require("express");
const multer = require("multer");
var cors = require("cors");
const app = express();
var storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, "public/");
  },
  filename: function (req, file, cb) {
    let ext = file.originalname.split(".");
    ext = ext[ext.length - 1];
    cb(null, `${Date.now()}.${ext}`);
  }
});

const upload = multer({ storage: storage });
var corsOptions = {
  origin: "http://localhost:3000",
  optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
  credentials: true
};

app.use([
  express.static("public"),
  express.json(),
  cors(corsOptions),
  upload.array("files")
]);

app.post("/upload_files", (req, res) => {
  // console.log(req.body);
  if (req.files.length > 0) {
    res.json(req.files[0]);
  }
});

app.listen(8080, () => {
  console.log(`Server started...`);
});

Editor.js

import React from "react";
import { CKEditor } from "@ckeditor/ckeditor5-react";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";

const API_URL = "http://localhost:8080";
const UPLOAD_ENDPOINT = "upload_files";

export default function Editor({ handleChange, SetContent }) {
  function uploadAdapter(loader) {
    return {
      upload: () => {
        return new Promise((resolve, reject) => {
          const body = new FormData();
          loader.file.then((file) => {
            body.append("files", file);
            fetch(`${API_URL}/${UPLOAD_ENDPOINT}`, {
              method: "post",
              body: body
            })
              .then((res) => res.json())
              .then((res) => {
                resolve({
                  default: `${API_URL}/${res.filename}`
                });
              })
              .catch((err) => {
                reject(err);
              });
          });
        });
      }
    };
  }


  function uploadPlugin(editor) {
    editor.plugins.get("FileRepository").createUploadAdapter = (loader) => {
      return uploadAdapter(loader);
    };
  }


  return (
    <div className="form-wrapper">
      <CKEditor className='editor'
        config={{
          extraPlugins: [uploadPlugin]
        }}
        data="<p>게시글을 작성해주세요</p>"
        editor={ClassicEditor}
        onReady={(editor) => {}}
        onBlur={(event, editor) => {}}
        onFocus={(event, editor) => {}}
        // onChange={(event, editor) => {
        //   handleChange(editor.getData())
        //   console.log(editor.getData())
        // }}
        onChange={(event, editor) => {
          const data2 = editor.getData();
          SetContent(data2)
          console.log(data2)
        }}
      />
    </div>
  );
}

Write.js

내용창에다가 입력을 하게 되면 console창에
<p>내용을입력하세요</p>
로 입력이 되어서 게시글을 띄울때는 html-react-parser를 사용해서 띄워주웠다

import React, { useState } from 'react';
import './Write.css';
import Edeitor from '../../components/Editor/Editor'
import Category from '../../components/Select/Select'
import NavBell from '../../components/NavBell/NavBell'
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import TextField from '@mui/material/TextField';
import parse from 'html-react-parser';


export default function Write() {
  const [title, SetTitle] = useState('제목')
  const [Content, SetContent] = useState('')
  const [editor, setEditor] = useState(null);

  const changeTitle = (e) => {
    SetTitle(e.target.value)
    console.log(e.target.value)
  }

  const changeContent = (e) => {
    SetContent(e.target.value)
    console.log(e.target.value)
  }


  return (
    <>
      {/* <div>{title}</div> */}
      <div>{parse(Content)}</div>
      <NavBell />
      <div className="container">
        <TextField id="outlined-basic" value={title} label="제목을 입력하세요" variant="outlined" onChange={changeTitle} />
        <Category />
        <Edeitor
          SetContent={SetContent}
          // handleChange={(data) => {
          //   setEditor(data);
          // }}
          data={Content}
        />
        <p>
          <Stack direction="row" spacing={2}>
            <Button variant="outlined">미리보기</Button>
            <Button variant="outlined">임시저장</Button>
            <Button variant="outlined">완료</Button>

          </Stack>
        </p>
      </div>

    </>
  )
}

App.js

import { BrowserRouter, Switch, Route, Link } from "react-router-dom";
import View from './pages/View/View';
import Write from '../src/pages/Write/Write'



function App() {
  return (
    <BrowserRouter>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/Write">작성</Link>{/* Link 컴포넌트를 이용하여 경로를 연결합니다 */}
            </li>
            <li>
              <Link to="/view"></Link>
            </li>
          </ul>
        </nav>

        <Switch>
          <Route exact path="/Write">
            <Write />
          </Route>
          <Route path="/view">
            <View />
          </Route>
        </Switch>
      </div>
    </BrowserRouter>
  );
}



export default App;

dependencies(package.json)

파일을 만들면서 필요한 다른 디펜던시도 같이 들어가 있다.

  "dependencies": {
    "@ckeditor/ckeditor5-build-classic": "^30.0.0",
    "@ckeditor/ckeditor5-react": "^3.0.3",
    "@emotion/react": "^11.4.1",
    "@emotion/styled": "^11.3.0",
    "@fortawesome/fontawesome-svg-core": "^1.2.36",
    "@fortawesome/free-brands-svg-icons": "^5.15.4",
    "@fortawesome/free-regular-svg-icons": "^5.15.4",
    "@fortawesome/free-solid-svg-icons": "^5.15.4",
    "@fortawesome/react-fontawesome": "^0.1.15",
    "@mui/material": "^5.0.2",
    "@testing-library/jest-dom": "^5.14.1",
    "@testing-library/react": "^11.2.7",
    "@testing-library/user-event": "^12.8.3",
    "axios": "^0.21.4",
    "html-react-parser": "^1.4.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-redux": "^7.2.5",
    "react-router-dom": "^5.3.0",
    "react-scripts": "4.0.3",
    "redux": "^4.1.1",
    "redux-thunk": "^2.3.0",
    "web-vitals": "^1.1.2"
  },

참고주소 (본 글이 정말 30개는 넘는거 같다)

(1) https://medium.com/swlh/ckeditor5-with-custom-image-uploader-on-react-67b4496cb07d
(2) https://www.techgalery.com/2021/05/how-to-use-react-ckeditor-upload-file.html
(함수형 Simpler adapter 코드)
(3) https://falaner.tistory.com/60?category=898434

(4) 15개 top editor
초기에는 SummerNote로 진행하려 하였으나 Ckeditor5의 UI가 더 마음에 들어
사용하기로 결정했다.
하지만 다음번에 에디터를 쓰게 된다면 SummerNote도 사용해보고싶다
어디서 봤는데 한국인 개발자분들이 만드신거라는 글을 본거 같다
https://ourcodeworld.com/articles/read/1065/top-15-best-rich-text-editor-components-wysiwyg-for-reactjs


쉬울거라 생각하고 했지만 역시나 쉽게 얻을 수 있는것은 없었다
multer도 코드를 작성하며 처음 만나게 되어서 그 부분에서도 추가적으로 공부를 해야겠다.

0개의 댓글