All Articles

(NodeJS)Dropzone.js, multer를 활용한 이미지 업로드

Dropzone.js 소개

Dropzone.js

Dropzone.js는 프론트 페이지에서 사용하는 드래그드롭 라이브러리이다. 아래와 같이 생겼다.


Backend에서는 Dropzone을 통해 받은 파일을 처리해서 DB에 저장하면 된다.

알아두어야 하는 것들

Dropzone을 구현하면서 알아두어야 할 것

  • Dropzone의 기본설정은 파일을 올리는 순간 서버로 보낸다. 이를 방지하기 위해 옵션을 설정해 주어야 한다.
  • Dropzone에서 여러 개 파일을 동시에 올리는 것은 기본 값이 아니다.
  • Dropzone에서 바로 보내므로 header 값을 설정해주어야 한다. 특히 jwt를 사용할 경우에는 Bearer 토큰을 header에 붙여주어야 한다.
  • 만약 추가로 formData에 값을 집어넣어 req.body에서 사용하고 싶다면 메소드 안에 따로 append해서 저장해 두어야 한다.
  • 중복된 파일을 올리지 못하게 막으려면 따로 함수를 붙여주어야 한다.
  • **새로운 파일을 추가할때만 파일을 보낼 수 있으므로, 만약 기존에 있던 파일이 남아있고 업데이트를 해주어야 하는 상황이라면 다른 방식으로 파일을 보내주어야 한다.
<form action="" class="dropzone" method="post" enctype="multipart/form-data" id="dragUpload">
	<div class="fallback">
		<input name="file" name="files" multiple />
	</div>
</form> 		    

html에 Dropzone form을 만들어주고 method와 enctype을 설정해준다.

var myDropzone = new Dropzone(".dropzone", {
    url: 'http://localhost:8080/card/upload',
    method: "post",
    autoProcessQueue: false, //자동으로 보내기 방지
    paramName: 'files',
    parallelUploads: 99,
    maxFilesize: 10, // MB
    uploadMultiple: true,
    headers: {
        "Authorization": 'Bearer ' + token //localstorage에 저장된 token
    },
});

그리고 Dropzone 인스턴스를 생성해서 사용해 주면 되는데,
autoProcessQueue에 false를 붙여주지 않으면 자동으로 진행된다.
parallelUploads에는 여러 개를 동시에 보내는 옵션인데 기본값이 2로 되어있으므로 여러개를 보내고 싶다면 임의의 값을 지정해주어야 한다.

init: function() {
    var myDropzone = this;
    this.on("sending", function(file, xhr, formData) {
        let cardName = $("input[name='cardname']").val();
        formData.append("name", cardName);
    });

    this.on("queuecomplete", function(file) {
        document.location.href = "./cardnews.html" //업로드가 완료되었다면 이전 화면으로 이동
    });
    this.on("addedfile", function(file) { //중복된 파일의 제거 
        if (this.files.length) {
            // -1 to exclude current file
            var hasFile = false;
            for (var i = 0; i < this.files.length - 1; i++) {
                if (this.files[i].name === file.name && this.files[i].size === file.size &&
                    this.files[i].lastModifiedDate.toString() === file.lastModifiedDate.toString()) {
                    hasFile = true;
                    this.removeFile(file);
                }
            }
            if (!hasFile) {
                addedFiles.push(file);
            }
        } else {
            addedFiles.push(file);
        }
    });

  
    $('#btn_confirm').click(function(e) {
        let cardName = $("input[name='cardname']").val();
        if (cardName.length == 0) {
            alert('카드제목 입력 필요')
            return;
        }
        if (myDropzone.getRejectedFiles().length > 0) {
            let files = myDropzone.getRejectedFiles();
            alert('거부된 파일이 있습니다.', files)
            return;
        }

        e.preventDefault();
        e.stopPropagation();

        myDropzone.processQueue();
    })
}

위에서 보는 것처럼 #btn_confirm을 통해 저장을 눌러서 myDropzone.processQueue();를 호출해주어야 dropzone 라이브러리가 동작한다.

올리고 카드를 수정할 시

이 부분이 참 고민을 많이 했던 부분.

처음 카드를 올릴때 Dropzone으로 파일을 보내기가 가능했다고 하지만, 카드를 수정할 때의 경우에는 다르게 접근이 필요하다.

위에서 보듯이 어떤 파일은 추가가 될 것이고 어떤 파일은 삭제가 된다. 어떨때는 순서만 바뀔 수도 있다. 그렇기 때문에 새로운 파일을 추가할 때만 사용할 수 있는 Dropzone 라이브러리의 sending기능은 사용할 수 없다. 따라서 xhr로 파일을 직접 보내야 한다.

  1. 페이지를 열었을 때 API를 통해 가져오는 첫 데이터는 sort_row에서 추출한다.
  2. 모든 작업이 끝나고 저장 버튼을 누를 때 sort_row에서 값들을 추출한다.
  3. 백엔드에서는 값들이 온전하게 다 들어가 있는 경우에는 손대지 않고
  4. 값이 없는 애들 (filename, path)가 없는 경우는 xhr로 받아온 파일에서 값을 매핑해준다.
<li class="sort_row sort_row${idx}" data-filename="${subCard[idx].SUBCARD_FILENAME}">
    <div class="sortNumber">${subCard[idx].SUBCARD_ORDER}</div>
    <div><img src="images/draggable.png" alt="draggable icon"></div>
    <div class="thumbnail" style="background-image: url('http://localhost:8080/${fileURL}')"></div>
    <div class="img_zoom">
        <p>이미지 크게보기</p>
        <div class="zoom">
            <img src="http://localhost:8080/${fileURL}">
        </div>
    </div>
    <div class="file_name" id="${subCard[idx].SUBCARD_FILEORIGIN_NAME}">${subCard[idx].SUBCARD_FILEORIGIN_NAME}</div>
    <div class="item_delete">
        <img src="images/delete-forever.png" alt="delete">
    </div>
</li>

즉 시작하게 되면 저 sort row에서 API를 붙여 만들게 되는데, 수정이 모두 끝난 후 data를 백엔드로 보내 처리할 때 저장 당시의 sort row에서 값을 가져와 보내면 된다.

let fileArr = [];
let sortRow = document.querySelectorAll('.sort_row');
sortRow.forEach(el => {
    fileArr.push({
        originalname: $(el).children('.file_name').text(),
        order: $(el).children('.sortNumber').text(),
        path: 'uploads/' + $(el).data('filename'), //없으면 undefined
        filename: $(el).data('filename') //없으면 undefined
    });
});

이제 이 값 fileArr과 dropzone을 통해 받아온 newFiles을 xhr로 보내주면 된다.

/**
 * @param {String} cardName : 카드제목
 * @param {array} fileArr : 저장당시의 카드 데이터들
 * @param {array} newFiles : Dropzone을 통해 추가한 파일들
 */
const updateCardNews = async (cardName, fileArr, newFiles) => {
    let urlIDX = URLParser();
    var formData = new FormData();

    let xhr = new XMLHttpRequest();
    xhr.open('post', `${URL}/card/${urlIDX}`);
    xhr.setRequestHeader('Authorization', 'Bearer ' + token);
    // xhr.setRequestHeader('Content-Type', 'multipart form-data'); 붙이면 안간다. 왜지?

    let jsonfiles = JSON.stringify(fileArr);
    let jsonName = JSON.stringify(cardName);

    formData.append('fileArr', jsonfiles);
    formData.append('cardName', jsonName);

    for (let index in newFiles) {
        formData.append('files', newFiles[index]);
    }

    xhr.send(formData);
    xhr.onreadystatechange = function() {
        if (xhr.readyState === xhr.DONE) {
            if (xhr.status === 200 || xhr.status === 201) {
                window.location.href = 'cardnews.html';
            } else {
                console.error("card Update 결과값을 받아오지 못함", xhr.response);
            }
        }
    };
};

파일을 이렇게 보내준다. 헷갈렸던 것은 21번 라인 부분인데, newFiles를 File 타입으로 받아오더라도 각각의 파일을 formData에 appand 시켜줘야 한다.

xhr.setRequestHeader('Content-Type', 'multipart form-data'); 를 xhr에 붙이게 되면 값이 들어가지 않는다. 이유가 뭘까?

서버측 처리 방법

애먹었었던 카드를 업데이트 하는 부분이다.

let fileArr = req.body.fileArr;
let newFiles = req.files;

fileArr = JSON.parse(fileArr);

fileArr.forEach(el => {
    if (el.filename === undefined) {
        newFiles.forEach(file => {
            if (el.originalname === file.originalname) {
                el.filename = file.filename;
                el.path = file.path;
            }
        });
    }
});

보면 fileArr부분에서 코드 처리 부분이다.
우리가 받은 파일 중 새로올린 파일은 filename과 path가 없는 상태이다.

  1. 최종 파일 데이터인 fileArr을 돌면서 배열에 filename이 있는지 확인한다.
  2. 파일이 undefined로 되어 있다면?
  3. 새로 받아온 newFiles에서 originalName이 같은 것을 확인한다.
  4. 없는 엘리먼트에 newFile의 이름과 path를 넣어준다.