동기
글 중간중간에 사진을 넣어가면서 내용을 구성할 수 있는 게시판이 만들고 싶었다.
진행 과정
CKEditor Documentation을 적극 활용했다. 많은 사람들이 쓰는 웹 에디터인 만큼 공식 문서가 엄청 친절하게 잘 되어있다. 리액트 컴포넌트로 CKEditor를 사용하는 방법도 여기를 보면 그냥 한눈에 알 수 있다. 전달할 수 있는 props의 수가 매우 많아서 아직 다 파악하지는 못했지만 간단하게 사용하기에는 문제가 없을 것 같다. 하면서 가장 어려웠던 건 이미지를 CKEditor에 붙여넣기해서 넣는 것이었다. 이미 어딘가에 호스팅이 되어 있는 이미지 같은 경우에는 CKEditor가 알아서 src를 잡아서 img 태그로 내용을 만들어줬지만, 로컬에서 파일을 복사해서 넣을 경우에는 프리미엄 버전인 superbuild에서는 Base64로 인코딩된 이미지를 포스트 내용 자체에 포함할 수 있었지만, 내가 사용할 무료 버전인 classic에서는 이게 불가능했다. 따라서 내가 별도의 파일 서버를 만들어야 했다. 이전에 스프링으로 파일 서버를 만들어 본 경험이 있었기에 오래 걸리지는 않았다. 그러고 이미지를 붙여넣을 때 파일 서버에 업로드를 진행해주는 어댑터를 Deep dive - custom upload adapter를 보고 만들어주었다. 이렇게 하여 내가 만든 Editor 컴포넌트는 다음과 같다.
import React, { useRef, useState } from "react";
import { CKEditor } from "@ckeditor/ckeditor5-react";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
import axios from "axios";
import { useNavigate } from "react-router-dom";
import MyButton from "./MyButton";
const Editor = () => {
const navigate = useNavigate();
const titleRef = useRef();
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const customUploadAdapter = (loader) => {
return {
upload() {
return new Promise((resolve, reject) => {
const formData = new FormData();
loader.file.then((file) => {
formData.append("file", file);
axios
.post("http://localhost:8080/api/v0/file/upload", formData)
.then((res) => {
resolve({
default: res.data.data.uri,
});
})
.catch((err) => reject(err));
});
});
},
};
};
function uploadPlugin(editor) {
editor.plugins.get("FileRepository").createUploadAdapter = (loader) => {
return customUploadAdapter(loader);
};
}
const handleSubmit = () => {
if (title.length < 1) {
titleRef.current.focus();
return;
}
const data = {
title,
content,
};
axios.post("http://localhost:8080/api/v0/post", data).then((res) => {
if (res.status === 200) {
navigate("/", { replace: true });
return;
} else {
alert("업로드 실패.");
return;
}
});
};
return (
<div className="Editor">
<section>
<div className="title-wrapper">
<textarea
className="input-title"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="제목을 입력하세요"
ref={titleRef}
/>
</div>
</section>
<section>
<CKEditor
editor={ClassicEditor}
data=""
config={{ extraPlugins: [uploadPlugin] }}
onReady={(editor) => {
// You can store the "editor" and use when it is needed.
console.log("Editor is ready to use!", editor);
}}
onChange={(event, editor) => {
setContent(editor.getData());
console.log({ event, editor, content });
}}
onBlur={(event, editor) => {
console.log("Blur.", editor);
}}
onFocus={(event, editor) => {
console.log("Focus.", editor);
}}
/>
</section>
<section>
<div className="control-box">
<div className="cancel-btn-wrapper">
<MyButton
text={"취소"}
onClick={() => navigate(-1, { replace: true })}
/>
</div>
<div className="submit-btn-wrapper">
<MyButton text={"완료"} type="black" onClick={handleSubmit} />
</div>
</div>
</section>
</div>
);
};
export default Editor;
결과물
아래 사진이 결과물인데...기능 위주로 만들어서 디자인은 구리다 ㅋㅋㅋ
업로드 하고 나서 해당 게시글 상세 페이지로 들어가면 아래와 같이 내용을 볼 수 있다.
후기
리액트는 확실히 간편한 라이브러리라는 것을 느꼈다. 컴포넌트 위주로 분리해서 개발하니까 페이지를 html 파일로 쭉 만들 때보다 훨씬 생각 정리가 잘 되는 것 같았다. 그리고 스프링으로 서버를 만들려니까 간단한 서버인데도 코드가 많이 길어지긴 하는 것 같아서 다음 포스트부터는 생산성을 위해 주로 Express 서버를 써볼까 한다. 여튼 뭔가 어려운 걸 해본 건 아니지만 프론트엔드는 늘 손 놓고 있었어서 이걸로 뭘 만들어 본다는 게 설레고 재미있었다. 오늘 포스트는 이까지 하고 다시 리액트로 다른 걸 해보러 가야겠다. 열코!
'웹 개발 > React' 카테고리의 다른 글
[React] React에서 chart.js 사용해보기 (0) | 2023.04.08 |
---|