포스트

Toast ui Editor 이미지 업로드 구현방법


지난 글에서는 Toast ui Editor(Tui)를 CDN 방식으로 불러오는 방법에 관한 글에 이어서 이미지 업로드 기능을 구현하는 방법을 알아보기위해 작성한 글이다.

Tui는 기본적으로 이미지 업로드 기능을 지원한다. 이미지를 업로드할 경우 base64의 형태로 변환하여 이미지를 업로드 한다. 이 기능은 치명적인 단점이 있는데, 이미지의 용량이 커질수록 변환된 base64 텍스트의 길이는 기하급수적으로 높아진다. 만약 base64로 변환된 이미지 그대로 DB에 저장한다고 했을 경우, 엄청난 길이의 텍스트로 인해 DB에 심한 부담을 줄 것이다.

따라서 사용자가 이미지를 업로드할 경우 이미지를 서버에 저장하고 저장된 이미지의 주소를 리턴하여 이미지가 출력되도록 해야한다. Spring을 사용하여 이미지를 서버에 저장하고 주소를 리턴하도록 해보자.


1. 이미지를 업로드 했을 때 콜백함수로 로직구현


Tui 공식문서에서는 에디터를 사용할 때 특정 행동을 할 때 콜백함수가 작동되도록하는 hooks라는 함수를 지원한다. hooks에는 여러 hook이 존재하는데, 그 중 addImageBlobHook을 사용하면 이미지를 업로드 했을 때 콜백함수가 작동하도록 할 수 있다.

먼저 Editor를 불러올 때 작성한 코드에 addImageBlobHook을 추가해보자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const editor = new toastui.Editor({
  el: document.querySelector("#editor"),
  height: "500px",
  initialEditType: "wysiwyg",
  placeholder: "내용을 입력해주세요",
  hooks: {
    addImageBlobHook: function (blob, callback) {
      const formData = new FormData();
      formData.append("image", blob);
      formData.append("uri", window.location.pathname);
      const imageURL = imageUpload(formData);
      callback(imageURL, "image");
    },
  },
  language: "ko-KR",
});

function imageUpload(formData) {
  let imageURL;

  $.ajax({
    type: "post",
    url: "/bombom/image_upload.do",
    async: false,
    data: formData,
    processData: false,
    contentType: false,
    success: function (data) {
      imageURL = data;
      console.log(imageURL);
    },
    error: function (request, status, error) {
      alert(request + ", " + status + ", " + error);
    },
  });

  return imageURL;
}


addImageBlobHook hook을 사용할 경우 작동할 함수를 작성해주었다. 파라미터로는 blob과 callback이 들어가게되는데, blob은 사용자가 업로드하려고 하는 이미지, 콜백은 비즈니스 로직이 수행된 다음에 사용자에게 리턴할 이미지 URL을 리턴해주면 에디터에서 이미지를 출력하게되는 원리이다.

서버에 이미지를 업로드해야하므로 ajax를 사용하여 서버로 이미지를 넘기도록 하였다. post방식으로 서버에 데이터를 넘기게 되고, form 태그를 통해 보통 데이터를 넘기지만 여기서는 form 태그를 사용할 수 없기 때문에 JavaScript에서 제공하는 formData 객체를 사용하여 해당 객체 안에 데이터를 넣어서 전달하도록 하였다.

async를 true로 설정했을 때 데이터가 넘겨지지 않아서 false로 설정하고 넘겼더니 문제없이 잘 구동 되었다. 아마 비동기일 때 사용자가 데이터를 넘기는 시점이 달라서 안되는 것 같다. async를 false로 사용할 때 비권장된다는 경고메시지가 나오긴하지만 일단 기능구현이 되므로 이렇게 사용하기로 했다.


2. 서버에서 전달받은 이미지 저장하기

이제 Controller에서 이미지를 어떻게 처리하는지 코드를 통해 알아보자,


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import java.io.File;
import java.io.IOException;
import java.time.LocalDate;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

@Controller
public class EditorController {

	private Logger logger = LoggerFactory.getLogger(this.getClass());

	/**
	 * 파일이름 추출하기
	 * 1. 업로드한 파일의 확장자명 알아내기
	 * 2. (오늘날짜)_currentTimeMillis + 확장자 방식으로 파일명 생성
	 * 3. 파일 경로에 등록
	 * 4. 주소값 리턴 (https://localhost:8080/bombom/resources/upload/파일)
	 */
	@ResponseBody
	@RequestMapping(value = "/image_upload.do", method = RequestMethod.POST)
	public String imageUpload(@RequestParam("image")MultipartFile multipartFile,
							  @RequestParam String uri, HttpServletRequest request) {

		if(multipartFile.isEmpty()) {
			logger.warn("user_write image upload detected, but there's no file.");
			return "not found";
		}

		String directory = request.getSession().getServletContext().getRealPath("resources/upload/talk/");

		String fileName = multipartFile.getOriginalFilename();
		int lastIndex = fileName.lastIndexOf(".");
		String ext = fileName.substring(lastIndex, fileName.length());
		String newFileName = LocalDate.now() + "_" + System.currentTimeMillis() + ext;

		try {
			File image = new File(directory + newFileName);

			multipartFile.transferTo(image);

		} catch (IllegalStateException | IOException e) {
			e.printStackTrace();
		} finally {
			logger.info("uri : {}", uri);
			logger.info("Image Path : {}", directory);
			logger.info("File_name : {}", newFileName);
		}

		// 주소값 알아내기
		String path = request.getContextPath();
		int index = request.getRequestURL().indexOf(path);
		String url = request.getRequestURL().substring(0, index);

		// https://localhost:8080/bombom/resources/upload/파일이름

		return url + request.getContextPath() + "/resources/upload/talk/" + newFileName;
	}
}


로직은 크게 4개로 나눌 수 있다.

  1. @RequestParam 어노테이션을 사용하여 이미지 가져오기
  2. 이미지를 저장할 경로 설정하기
  3. 저장할 이미지의 이름 재설정하기
  4. 이미지를 서버 경로에 저장하기
  5. 주소값 리턴하기

이미지는 보통 MultipartFile 객체를 통해 받아오게 된다. @RequestParam 어노테이션을 사용하여 이미지를 받아와서 비즈니스 로직에 사용할 수 있도록 했다.

그 다음은 경로를 지정해주어야하는데, 경로는 HttpServletRequest 객체를 사용하여 getSession(), getServletContext(), getRealPath() 메서드를 사용하여 서버 내에 저장할 경로를 지정해 주었다.

파일이름은 이름이 중복되는 것을 피하기 위해서 오늘 날짜 + currentTimeMillis() 메서드를 사용해주었다.

위의 세 절차를 모두 완료했다면, 경로와 파일이름으로 된 File 객체를 생성하고, transferTo() 메서드를 사용하여 이미지를 지정한 경로로 이동시켜준다.

마지막으로 HttpServletRequest 객체를 사용하여 메서드를 사용하여 리턴할 주소값을 가공하여 전달하면 된다.


3. 결과

현재 진행중인 봄봄프로젝트에서 구현한 게시판 글쓰기 기능이다. 이미지 업로드와 글 등록까지 정상적으로 수행되는 것을 볼 수 있다. 글 등록은 따로 Controller를 만들어서 로직을 처리했기 때문에 만약 이미지 업로드가 잘 된다면 글 등록까지 무난하게 구현할 수 있을 것이라고 생각한다.

에디터를 사용하면서 이미지 업로드를 어떻게 처리해야할지 막막했다. 열심히 구글링을 하면서 찾아본 결과 끝내 구현할 수 있었다. 대부분의 글들은 리액트를 사용하기 때문에 작성하는 코드가 내가 구현하려는 것과 많이 달랐지만, 공식문서와 같이 참조하면서 삽질한 결과 기능을 구현할 수 있었다.

이 포스트는 저작권자의 CC BY 4.0 라이센스를 따릅니다.