아래 글에서 프론트엔드에 웹 에디터를 적용한 후, 본문 이미지를 처리하면서 생긴 문제를 해결한 방법을 기록한 포스팅 입니다.
아래 글을 먼저 보시고 오시면 이해에 도움이 되니 참고바랍니다.
https://byunsw4.tistory.com/46
1. react-quill 을 이용해 본문 이미지를 삽입하는 경우 발생하는 문제
웹 에디터의 주요 기능 중 하나인 "본문 이미지" 기능을 react-quill 를 통해 사용하는 경우, 아래와 같이 본문 정보를 처리하게 됩니다.
본문 이미지 자체를 base64 로 인코딩한 형태로 그대로 보존하고 있는 형태입니다.
물론, 이대로 본문 정보를 저장한다면 별도의 처리 없이 등록한 이미지 그대로를 화면에서 볼 수 있게 됩니다.
위와 같이 저장해도 상관없어 보이지만, 상품 정보를 보기좋게 설명하기 위해 여러 개의 이미지를 등록하는 경우, 데이터 조회 쿼리가 엄청 느려질 수 있습니다.
2. 본문 이미지 임시저장
앞서 본 본문 이미지 정보에 대한 문제를 해결하기 위해 본문 이미지를 미리 서버에 저장하는 방식으로 본문 이미지 처리 방식을 수정하기로 했습니다.
- 에디터에 이미지가 등록되면, 특정 API 를 통해 이미지파일을 multipart 형태로 전송한다.
- 요청으로 들어온 파일을 저장하고, 저장된 파일을 불러오기 위한 URI 경로를 응답으로 내보낸다.
- 응답으로 전달된 경로를 <img> 태그에 src 속성으로 대체한다.
위 기능을 구현하기 위해 백엔드를 아래와 같이 구현했습니다.
@RequiredArgsConstructor
@RestController
public class FileController {
private final FileHandler fileHandler;
/* == 본문 이미지 임시저장 == */
@PostMapping("/files/temp")
public ResponseEntity<FileSaveResponse> tempFileSave(@RequestPart(name = "file") MultipartFile file) {
String storeFileName = fileHandler.saveTempContentImageFile(file);
String tempContentImageURI = "/content/img/temp/" + storeFileName;
return ResponseEntity.ok(new FileSaveResponse(tempContentImageURI));
}
/* == 임시저장한 이미지 파일을 불러옴 == */
@GetMapping("/content/img/temp/{storedFileName}")
public Resource getTempContentImage(@PathVariable("storedFileName") String storeFileName) throws MalformedURLException {
String tempContentImagePath = fileHandler.getTempContentImagePath(storeFileName);
return new UrlResource("file://" + tempContentImagePath);
}
}
@Component
public class FileHandler {
private static final String CONTENT_IMAGE_TEMP_DIR = "~~~";
private static final String CONTENT_IMAGE_PROD_DIR = "~~~";
public String getTempContentImagePath(String storeFileName) {
return CONTENT_IMAGE_TEMP_DIR + storeFileName;
}
public String saveTempContentImageFile(MultipartFile file) {
return save(file, CONTENT_IMAGE_TEMP_DIR);
}
private String save(MultipartFile file, String path) {
// 물리파일 저장 로직 태우기 - 임시저장은 임시경로에 물리파일만 저장한다.
if(file.isEmpty()) {
throw new IllegalArgumentException("파일 없음.");
}
try {
String realFileName = file.getOriginalFilename();
if(!StringUtils.hasText(realFileName)) {
throw new IllegalArgumentException("파일명 없음.");
}
String storeFileName = UUID.randomUUID().toString();
String tempPath = path + storeFileName;
file.transferTo(new File(tempPath));
return storeFileName;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Editor 부분은 아래와 같이 웹 에디터의 이미지삽입 버튼 이벤트를 재정의하는 방식으로 개발했습니다.
코드 출처 : https://mingeesuh.tistory.com/entry/Quill-React-%EC%97%90%EB%94%94%ED%84%B0-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-%EB%B0%8F-%EC%82%AC%EC%9D%B4%EC%A6%88-%EC%A1%B0%EC%A0%88
import React, { useEffect, useRef } from "react";
import ReactQuill from "react-quill";
import 'react-quill/dist/quill.snow.css'
import FileService from "../../../js/file";
import { webUrl } from "../../../js/axios";
const Editor = ({editorValue, onChangeEditorValue}) => {
const quillRef = useRef();
const formats = [
"size", "color", "background", "bold", "italic",
"underline", "strike", "blockquote", "list",
"bullet", "indent", "link", "image",
];
const toolbarOptions = [
[{ size: ["small", false, "large", "huge"] }], // custom dropdown
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
["bold", "italic", "underline", "strike", "blockquote"],
[
{ list: "ordered" },
{ list: "bullet" },
{ indent: "-1" },
{ indent: "+1" },
],
["link", "image"],
];
const uploadImage = async (file) => {
let result = await FileService.saveTempFile(file);
return webUrl + result.tempContentImageURI;
}
useEffect(() => {
const handleImage = () => {
const input = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("accept", "image/*");
input.click();
input.onchange = async () => {
const editor = quillRef.current.getEditor();
const file = input.files[0];
const range = editor.getSelection(true);
try {
const url = await uploadImage(file);
console.log(url);
// 받아온 url을 이미지 태그에 삽입
editor.insertEmbed(range.index, "image", url);
// 사용자 편의를 위해 커서 이미지 오른쪽으로 이동
editor.setSelection(range.index + 1);
} catch (e) {
editor.deleteText(range.index, 1);
}
}
}
if (quillRef.current) {
const toolbar = quillRef.current.getEditor().getModule("toolbar");
toolbar.addHandler("image", handleImage);
}
}, []);
return(
<ReactQuill
style={{height: "350px", margin: "4px" }}
ref={quillRef}
value={editorValue}
modules={{
toolbar: {
container: toolbarOptions,
}
}}
formats={formats}
onChange={onChangeEditorValue}
/>
);
}
export default Editor;
3. 적용 결과
위 개발내역을 적용한 후, 본문 이미지 등록 및 상품 정보 등록 시 아래처럼 데이터가 저장되는 것을 확인할 수 있었습니다.
4. 개선방안
위 형태로 개발하다 보니, 다음과 같은 문제점이 발생한다는 점을 알게되었습니다.
본문 이미지를 등록하고 실제로 데이터를 저장하지 않으면, 의미없는 이미지 파일이 서버에 저장된 채 남게된다.
위 문제를 해결하기 위해 아래 3가지의 개선사항이 필요하다는 생각이 들었습니다.
- 작성 단계에서 웹 에디터에 등록되는 이미지 파일은 temp 폴더에 저장한다.
- 실제로 작성한 내용을 저장하는 경우, 본문 이미지를 temp 폴더에서 prod 폴더로 복사한다. 또한, 본문에 삽입되는 Img 태그의 src 경로는 temp 폴더의 이미지를 불러오는 것이 아닌, prod 폴더의 이미지를 불러오는 URL 로 치환한다.
- 주기적으로 temp 폴더에 저장된 이미지 파일들을 삭제해준다.
본문 이미지 처리를 개선했지만, 위 개선사항들까지 개선되어야 할 것으로 보여 추가로 개발해야겠습니다.
'Refactoring' 카테고리의 다른 글
[나홀로 리팩토링] 카테고리 리팩토링 해보기 (0) | 2023.03.16 |
---|
댓글