본문 바로가기
Refactoring

본문 이미지 저장 로직을 개선해서 서버 성능 개선하기.

by 덩라 2024. 1. 28.

아래 글에서 프론트엔드에 웹 에디터를 적용한 후, 본문 이미지를 처리하면서 생긴 문제를 해결한 방법을 기록한 포스팅 입니다.

아래 글을 먼저 보시고 오시면 이해에 도움이 되니 참고바랍니다.

https://byunsw4.tistory.com/46

 

[React-Quill] 웹 에디터 적용기

사이드 프로젝트를 진행하면서 웹 에디터를 구현할 사항이 발생했습니다. 제 사이드 프로젝트의 Front 는 React 기반으로 되어있기에, React 에서 사용할 수 있는 웹 에디터를 찾던 중 react-quill 이라

byunsw4.tistory.com


1. react-quill 을 이용해 본문 이미지를 삽입하는 경우 발생하는 문제

웹 에디터의 주요 기능 중 하나인 "본문 이미지" 기능을 react-quill 를 통해 사용하는 경우, 아래와 같이 본문 정보를 처리하게 됩니다.

본문 이미지 자체를 base64 로 인코딩한 형태로 그대로 보존하고 있는 형태입니다.

물론, 이대로 본문 정보를 저장한다면 별도의 처리 없이 등록한 이미지 그대로를 화면에서 볼 수 있게 됩니다.

상세조회 화면 결과
실제로 테이블에 insert 된 상품 정보 데이터

 

위와 같이 저장해도 상관없어 보이지만, 상품 정보를 보기좋게 설명하기 위해 여러 개의 이미지를 등록하는 경우, 데이터 조회 쿼리가 엄청 느려질 수 있습니다.

 

2. 본문 이미지 임시저장

앞서 본 본문 이미지 정보에 대한 문제를 해결하기 위해 본문 이미지를 미리 서버에 저장하는 방식으로 본문 이미지 처리 방식을 수정하기로 했습니다.

  1. 에디터에 이미지가 등록되면, 특정 API 를 통해 이미지파일을 multipart 형태로 전송한다.
  2. 요청으로 들어온 파일을 저장하고, 저장된 파일을 불러오기 위한 URI 경로를 응답으로 내보낸다.
  3. 응답으로 전달된 경로를 <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
 

Quill React 에디터 사용해보기 (이미지 업로드 및 사이즈 조절)

Quill Editor란? 공식문서에 따르면 Quill Editor는 모던 웹을 위한 오픈소스 WYSIWYG 에디터라고 소개하고 있다. WYSIWYG는 What You See Is What You Get의 약자로, 편집 과정에서의 화면 포맷이 최종 완성본이랑

mingeesuh.tistory.com

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. 적용 결과

위 개발내역을 적용한 후, 본문 이미지 등록 및 상품 정보 등록 시 아래처럼 데이터가 저장되는 것을 확인할 수 있었습니다.

기존 base64 형태의 data 형태가 아닌, 이미지를 get 요청으로 가져옴

 

본문 내용에서 img src 정보가 url 로 변경되어 insert 된 모습

 

4. 개선방안

위 형태로 개발하다 보니, 다음과 같은 문제점이 발생한다는 점을 알게되었습니다.

본문 이미지를 등록하고 실제로 데이터를 저장하지 않으면, 의미없는 이미지 파일이 서버에 저장된 채 남게된다.

 

 

위 문제를 해결하기 위해 아래 3가지의 개선사항이 필요하다는 생각이 들었습니다.

  1. 작성 단계에서 웹 에디터에 등록되는 이미지 파일은 temp 폴더에 저장한다.
  2. 실제로 작성한 내용을 저장하는 경우, 본문 이미지를 temp 폴더에서 prod 폴더로 복사한다. 또한, 본문에 삽입되는 Img 태그의 src 경로는 temp 폴더의 이미지를 불러오는 것이 아닌, prod 폴더의 이미지를 불러오는 URL 로 치환한다.
  3. 주기적으로 temp 폴더에 저장된 이미지 파일들을 삭제해준다.

본문 이미지 처리를 개선했지만, 위 개선사항들까지 개선되어야 할 것으로 보여 추가로 개발해야겠습니다.

'Refactoring' 카테고리의 다른 글

[나홀로 리팩토링] 카테고리 리팩토링 해보기  (0) 2023.03.16

댓글