관련지식
javascript, jquery, image-preview, file-upload, drag&drop, closure, multi-line-string, event-delegation-pattern

사진첩 같은 화면에 이미지를 업로드 하는 기능을 만들려고 합니다. 늘 사용하던 <input type="file"/> 말고 다른 형태로 해보고 싶네요. 그래서 아래와 같이 만들어보려고 합니다.

  1. 이미지를 Drag and Drop 으로 선택
  2. 선택한 이미지를 미리보기(업로드 안된 상태)

만들다보면 세세한 처리를 조금 더 해야하지만 위 두가지 기능을 주요 목표로 만들어보려고 합니다. 같이 보시죠.

파일 Drag & Drop 구현

파일을 브라우저에 그냥 Drag & Drop 할 경우 페이지 URL이 바뀔뿐 사이트에 적용되거나 하진 않습니다. 이 기능을 구현하기 위해선 특별한 영역을 지정해주어야 합니다.

  1. <div id="drop" style="border:1px solid black; width:800px; height:300px; padding:3px">
  2. 여기로 drag & drop
  3. </div>

화면을 보면 단순히 네모난 영역이 생겼을뿐 입니다. 이제 여기에 Drag & Drop 이 가능하도록 이벤트를 만들어주어야 합니다.

  1. var $drop = $("#drop");
  2. $drop.on("dragenter", function(e) { //드래그 요소가 들어왔을떄
  3. $(this).addClass('drag-over');
  4. }).on("dragleave", function(e) { //드래그 요소가 나갔을때
  5. $(this).removeClass('drag-over');
  6. }).on("dragover", function(e) {
  7. e.stopPropagation();
  8. e.preventDefault();
  9. }).on('drop', function(e) { //드래그한 항목을 떨어뜨렸을때
  10. e.preventDefault();
  11. $(this).removeClass('drag-over');
  12. });

DragEvent 에는 여러 종류가 있지만 이 기능을 구현할때는 위의 네가지 이벤트면 충분합니다. drop 이벤트가 발생한 시점이 Drag 한 요소를 Drop 한 시점입니다. 프리뷰 이미지를 만드는 로직이 들어가야 하는 위치 입니다.
dragover 이벤트 부분은 별로 필요가 없어보이지만 왜 필요한지는 한번 지워서 테스트 해보시면 될것 같습니다.

DragEvent 의 상세 이벤트는 아래 url을 참고하세요.

DragEvent : https://developer.mozilla.org/ko/docs/Web/API/DragEvent

이미지 미리보기 구현

먼저 drop 된 이미지 파일들이 어떤것인지 확인해봐야 할 것입니다.

  1. var files = e.originalEvent.dataTransfer.files;

e.originalEvent.dataTransfer.files 속성은 drag&drop 한 모든 파일들의 정보를 가진 FileList 객체입니다. 우리는 이 정보를 이용하여 미리보기를 만들수 있습니다. 주의하실 것은 FileList 는 배열과 닮았지만 배열이 아닌 유사배열이라는 것입니다.

FileList : https://developer.mozilla.org/ko/docs/Web/API/FileList

따라서 for 루프를 사용하겠습니다.

  1. for(var i = 0; i < files.length; i++) {
  2. var file = files[i];
  3. var size = uploadFiles.push(file); //업로드 목록에 추가
  4. preview(file, size - 1); //미리보기 만들기
  5. }
  6. function preview(file, idx) {
  7. var reader = new FileReader();
  8. reader.onload = (function(f, idx) {
  9. return function(e) {
  10. var div = '<div class="thumb"> \
  11. <div class="close" data-idx="' + idx + '">X</div> \
  12. <img src="' + e.target.result + '" title="' + escape(f.name) + '"/> \
  13. </div>';
  14. $("#thumbnails").append(div);
  15. };
  16. })(file, idx);
  17. reader.readAsDataURL(file);
  18. }

자바스크립트 개발이 익숙치 않은 분들에겐 preview() 함수 내용이 조금 어려울수 있으므로 처음보는 형태는 따로 내용을 찾아보시는 것이 좋을듯 합니다.

FileReader 는 Blob 데이터를 Data URL 로 바꿔줄수 있습니다. 혹시 Data URL 이란 용어가 생소한 분은 아래 그림을 참고하시면 됩니다. 이미지의 경로에 http URL이 아닌 이미지의 raw data를 base64로 인코딩한 문자열을 넣어도 화면에 보여질수 있습니다.

FileReader : https://developer.mozilla.org/ko/docs/Web/API/FileReader

reader.onload 에는 비동기 실행으로 변수값 유지를 위해 클로져(Closure)가 사용되었습니다.

문자열의 뒤에 ‘\’ 를 사용한것은 es5 형식의 멀티 라인 문자열을 의미합니다. ‘\’ 뒤에는 space를 포함한 아무런 문자가 없어야 합니다.

미리보기 삭제

위 까지 구현했으면 미리보기는 정상적으로 될 것입니다.(css는 최종 샘플 참고)

그런데 미리보기 한 이미지를 삭제하고 새로운 이미지를 더 추가하고 싶을수도 있습니다. 이번엔 미리보기 삭제 기능을 만들겠습니다.

  1. $("#thumbnails").on("click", ".close", function(e) {
  2. var $target = $(e.target);
  3. var idx = $target.attr('data-idx');
  4. uploadFiles[idx].upload = 'disable'; //삭제된 항목은 업로드하지 않기 위해 플래그 생성
  5. $target.parent().remove(); //프리뷰 삭제
  6. });

지금 만든 예제에서는 무수히 많은 미리보기 이미지를 넣기엔 무리가 있지만 실제로 서비스를 하는 기능에선 몇 개의 미리보기 이미지가 만들어질지 알 수 없습니다. 이렇게 이벤트를 걸어야 하는 대상이 많아질 때는 위의 소스와 같이 무조건 이벤트 델리게이션 패턴을 사용하는 것이 좋습니다.

미리보기에서 삭제를 했다고 uploadFiles 에서 삭제하진 않습니다. 삭제를 하면 인덱스의 위치가 바뀌기 때문에 삭제했다는 플래그(upload='disable')만 만듭니다.

파일 업로드

jquery를 지원하는 업로드 라이브러리를 이용할 수도 있겠지만 jquery.ajax 만으로 파일 업로드를 해보겠습니다. 특이할만한 부분은 HTML 상에 <form> 태그를 두지 않고 자바 스크립트에서 동적으로 생성하는 것입니다.

  1. $("#btnSubmit").on("click", function() {
  2. var formData = new FormData();
  3. $.each(uploadFiles, function(i, file) {
  4. if(file.upload != 'disable') //삭제하지 않은 이미지만 업로드 항목으로 추가
  5. formData.append('upload-file', file, file.name); //모든 첨부파일은 upload-file 이름으로 전달함
  6. });
  7. $.ajax({
  8. url: '업로드URL',
  9. data : formData,
  10. type : 'post',
  11. contentType : false,
  12. processData: false,
  13. success : function(ret) {
  14. alert("완료");
  15. }
  16. });
  17. });

‘미리보기 삭제’ 에서 언급한 것처럼 upload 속성이 ‘disable’인 것은 미리보기를 삭제한 것이므로 업로드 하지 않습니다.

ajax() 함수를 호출할 때 contentType과 processData 속성을 false 로 지정한것은 반드시 필요한 내용입니다. 흔히 파일 업로드를 할때 <form> 에 ‘enctype=”multipart/form-data”‘ 속성을 지정하는 것을 기억하실 겁니다. 그러나 위의 코드 어디에서도 enctype 을 지정한 부분은 없습니다. 그것을 위한 내용이 contentType이고 문자열이 아닌 데이터를 전달하기 때문에 processData도 사용하지 않습니다.

정리

여기까지 구현에 필요한 모든 로직을 살펴봤습니다. css 등 일부 로직이 빠져있는데 아래의 최종 샘플을 확인하시면 되겠습니다.

업로드 부분까지 언급하느라 내용이 길어졌는데 결국 DragEvent와 Data URL을 만드는 방법만 알면 미리보기 기능은 어렵지 않게 구현 가능합니다. 샘플을 참고해서 확인해 보시기 바랍니다.

제가 전문 퍼블리셔가 아니라 브라우저에 따라 화면이 좀 이상하게 보일수 있을것 같은데 크롬으로 확인해보시면 될것 같습니다.

최종 샘플

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <title>CSS Template</title>
  5. <meta charset="utf-8">
  6. <meta name="viewport" content="width=device-width, initial-scale=1">
  7. <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
  8. <style>
  9. .drag-over { background-color: #ff0; }
  10. .thumb { width:200px; padding:5px; float:left; }
  11. .thumb > img { width:100%; }
  12. .thumb > .close { position:absolute; background-color:red; cursor:pointer; }
  13. </style>
  14. </head>
  15. <body>
  16. <input type="button" id="btnSubmit" value="업로드"/>
  17. <div id="drop" style="border:1px solid black; width:800px; height:300px; padding:3px">
  18. 여기로 drag & drop
  19. <div id="thumbnails">
  20. </div>
  21. </div>
  22. <script>
  23. var uploadFiles = [];
  24. var $drop = $("#drop");
  25. $drop.on("dragenter", function(e) { //드래그 요소가 들어왔을떄
  26. $(this).addClass('drag-over');
  27. }).on("dragleave", function(e) { //드래그 요소가 나갔을때
  28. $(this).removeClass('drag-over');
  29. }).on("dragover", function(e) {
  30. e.stopPropagation();
  31. e.preventDefault();
  32. }).on('drop', function(e) { //드래그한 항목을 떨어뜨렸을때
  33. e.preventDefault();
  34. $(this).removeClass('drag-over');
  35. var files = e.originalEvent.dataTransfer.files; //드래그&드랍 항목
  36. for(var i = 0; i < files.length; i++) {
  37. var file = files[i];
  38. var size = uploadFiles.push(file); //업로드 목록에 추가
  39. preview(file, size - 1); //미리보기 만들기
  40. }
  41. });
  42. function preview(file, idx) {
  43. var reader = new FileReader();
  44. reader.onload = (function(f, idx) {
  45. return function(e) {
  46. var div = '<div class="thumb"> \
  47. <div class="close" data-idx="' + idx + '">X</div> \
  48. <img src="' + e.target.result + '" title="' + escape(f.name) + '"/> \
  49. </div>';
  50. $("#thumbnails").append(div);
  51. };
  52. })(file, idx);
  53. reader.readAsDataURL(file);
  54. }
  55. $("#btnSubmit").on("click", function() {
  56. var formData = new FormData();
  57. $.each(uploadFiles, function(i, file) {
  58. if(file.upload != 'disable') //삭제하지 않은 이미지만 업로드 항목으로 추가
  59. formData.append('upload-file', file, file.name);
  60. });
  61. $.ajax({
  62. url: '/api/etc/file/upload',
  63. data : formData,
  64. type : 'post',
  65. contentType : false,
  66. processData: false,
  67. success : function(ret) {
  68. alert("완료");
  69. }
  70. });
  71. });
  72. $("#thumbnails").on("click", ".close", function(e) {
  73. var $target = $(e.target);
  74. var idx = $target.attr('data-idx');
  75. uploadFiles[idx].upload = 'disable'; //삭제된 항목은 업로드하지 않기 위해 플래그 생성
  76. $target.parent().remove(); //프리뷰 삭제
  77. });
  78. </script>
  79. </body>
  80. </html>