관련지식
javascript, jquery, file-upload, xmlhttprequest, xhr, progress

이번엔 이전에 작성한 ‘[jquery] Drag and Drop 으로 이미지 파일 업로드’ 의 예시를 이용하여 파일 업로드 시 진행 상황을 표시하는 기능을 만들어 보겠습니다. 무한 반복재생하는 GIF 이미지를 사용하는 Indicator 같은 것이 아닌 실제로 업로드 전송량에 맞는 백분율 단위로 진행상황을 보여주는 방법입니다.

XMLHttpRequest.upload

흔히 AJAX로 알려진 XMLHttpRequest 약칭 XHR 은 하위 IE버전에도 존재하지만 최신 브라우저에서는 업그레이드 된 스펙을 구현하고 있고 그에 따라 upload 속성이 추가 되었습니다.

W3C 기술명세 : https://www.w3.org/TR/2012/WD-XMLHttpRequest-20120117/

추가된 upload 속성에 progress 이벤트를 등록하여 업로드/다운로드 상황에 대한 정보를 활용 할 수가 있습니다. 대략적인 사용법은 아래와 같습니다.

  1. xhr.upload.onprogress = function(e) {
  2. var percent = e.loaded * 100 / e.total;
  3. setProgress(percent);
  4. };

상세 이벤트 목록은 아래 링크를 참고하시면 됩니다.

XMLHttpRequest.upload : https://developer.mozilla.org/ko/docs/Web/API/XMLHttpRequest/upload

멀티 업로드에 대한 진행상황 확인

다건의 파일 업로드를 할 경우 모든 파일을 한번에 업로드하는 방법과 각 파일별로 업로드 하는 방법이 있습니다. 먼저 모든 파일을 한번에 업로드 하는 방법을 볼 것입니다.

진행상황을 표시하기 위해 아래와 같은 HTML을 추가할 것입니다.(전체 소스는 맨 밑에 있습니다.)

  1. <progress id="progressBar" value="0" max="100" style="width:100%"></progress>

<progress> 태그는 하위 브라우저에선 쓸 수 없지만 익스플로러 10부터는 이용가능한 유용한 태그입니다. 이번엔 업로드 기능을 포함한 전체 소스입니다.

  1. var formData = new FormData();
  2. $.each(uploadFiles, function(i, file) {
  3. if(file.upload != 'disable')
  4. formData.append('upload-file', file, file.name);
  5. });
  6. $.ajax({
  7. url: '/api/etc/file/upload',
  8. data : formData,
  9. type : 'post',
  10. contentType : false,
  11. processData: false,
  12. xhr: function() { //XMLHttpRequest 재정의 가능
  13. var xhr = $.ajaxSettings.xhr();
  14. xhr.upload.onprogress = function(e) { //progress 이벤트 리스너 추가
  15. var percent = e.loaded * 100 / e.total;
  16. setProgress(percent);
  17. };
  18. return xhr;
  19. },
  20. success : function(ret) {
  21. alert("완료");
  22. }
  23. });

여러 소스가 있지만 추가된 부분은 ajax를 호출하는 부분의 xhr 속성 하나 입니다. ajax에서 사용할 XMLHttpRequest 에 upload progress 에 대한 이벤트 리스너를 추가하면 업로드 진행 과정을 전달 받을 수 있습니다.

개별 업로드에 대한 진행상황 확인

파일 업로드 하는 방법은 정답이 정해져있지 않습니다. 한번에 모든 파일을 업로드하는 방법도 있지만 개개의 건별로 업로드를 하는 방법도 있습니다. 개개의 업로드를 동시에 진행하는 방법도 있죠. 여기선 개별 파일을 차례대로 업로드 하는 방법을 사용하겠습니다.

먼저 기존의 preview() 함수를 수정해야 합니다. 개별 파일의 진행상황을 알기위해 프리뷰 이미지에도 <progress> 태그를 추가합니다.

  1. reader.onload = (function(f, idx) {
  2. return function(e) {
  3. var $div = $('<div class="thumb"> \
  4. <progress value="0" max="100" ></progress> \
  5. <div class="close" data-idx="' + idx + '">X</div> \
  6. <img src="' + e.target.result + '" title="' + escape(f.name) + '"/> \
  7. </div>');
  8. $("#thumbnails").append($div);
  9. f.target = $div;
  10. };
  11. })(file, idx);

단순 문자열이었던 부분도 JQuery 객체로 변환 시켰는데 File 객체에 DOM 정보를 넣기 위한것입니다.

그다음은 개별 파일 업로드를 위한 함수 입니다. alert.bind 가 생소해 보일수 있지만 일단은 그냥 alert() 함수를 사용한 것이라 생각하시면 됩니다.

  1. function eachUpload() {
  2. var file = uploadFiles.shift();
  3. if(!file) {
  4. setTimeout(alert.bind(null, '완료'), 300);
  5. return;
  6. }
  7. if(file.upload == 'disable') {
  8. eachUpload();
  9. return;
  10. }
  11. var formData = new FormData();
  12. formData.append('upload-file', file, file.name);
  13. var $selfProgress = file.target.find("progress"); //File 객체에 저장해둔 프리뷰 DOM의 progress 요소를 찾는다.
  14. $.ajax({
  15. url: '/api/etc/file/upload',
  16. data : formData,
  17. type : 'post',
  18. contentType : false,
  19. processData: false,
  20. xhr: function() {
  21. var xhr = $.ajaxSettings.xhr();
  22. xhr.upload.onprogress = function(e) {
  23. var percent = e.loaded * 100 / e.total;
  24. $selfProgress.val(percent); //개별 파일의 프로그레스바 진행
  25. };
  26. return xhr;
  27. },
  28. success : function(ret) {
  29. uploadStatus.count++;
  30. setProgress(uploadStatus.count / uploadStatus.total * 100); //전체 프로그레스바 진행
  31. setTimeout(eachUpload, 500);
  32. }
  33. });
  34. }

ajax 의 옵션 자체는 이전 예제와 크게 다를것은 없습니다. 단지 개별 파일 업로드를 순차적으로 진행하기 위해 shift() 함수를 이용하여 약간의 요령을 부린것과 개별 프로그레스 진행과 전체 프로그레스 진행에 대한 로직이 있을 뿐입니다.

정리

이번에도 역시 다른 내용들이 있지만 결국 중요한것은 XMLHttpRequest.upload 속성에 progress 이벤트를 등록하면 업로드 중간중간 진행 이벤트를 받아볼 수 있다는 것입니다. 아래 최종 샘플은 완전히 동작하는 소스지만 안타깝게도 XMLHttpRequest.upload 를 모든 브라우저가 지원하진 않습니다.

하지만 익스플로러10 이상은 지원하고 꽤 많은 브라우저가 지원되므로 대고객용으로도 충분히 써먹을만한 기능입니다. 한번 테스트 해보시기 바랍니다. 참! 업로드할 이미지의 용량이 작으면 프로그레스 이벤트 발생횟수가 적어서 제대로 동작하는지 느낌이 잘 안옵니다. 사이즈가 좀 큰 이미지를 이용하세요!

최종 샘플

  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 > progress { width:100%; }
  13. .thumb > .close { position:absolute; background-color:red; cursor:pointer; }
  14. </style>
  15. </head>
  16. <body>
  17. <input type="button" id="btnSubmit" value="업로드"/>
  18. <div id="drop" style="border:1px solid black; width:800px; height:300px; padding:3px">
  19. 여기로 drag & drop
  20. <div id="thumbnails">
  21. <progress id="progressBar" value="0" max="100" style="width:100%"></progress>
  22. </div>
  23. </div>
  24. <script>
  25. var uploadFiles = [];
  26. var $drop = $("#drop");
  27. $drop.on("dragenter", function(e) { //드래그 요소가 들어왔을떄
  28. $(this).addClass('drag-over');
  29. }).on("dragleave", function(e) { //드래그 요소가 나갔을때
  30. $(this).removeClass('drag-over');
  31. }).on("dragover", function(e) {
  32. e.stopPropagation();
  33. e.preventDefault();
  34. }).on('drop', function(e) {
  35. e.preventDefault();
  36. $(this).removeClass('drag-over');
  37. var files = e.originalEvent.dataTransfer.files;
  38. for(var i = 0; i < files.length; i++) {
  39. var file = files[i];
  40. console.log(file);
  41. var size = uploadFiles.push(file);
  42. preview(file, size - 1);
  43. }
  44. });
  45. function preview(file, idx) {
  46. var reader = new FileReader();
  47. reader.onload = (function(f, idx) {
  48. return function(e) {
  49. var $div = $('<div class="thumb"> \
  50. <progress value="0" max="100" ></progress> \
  51. <div class="close" data-idx="' + idx + '">X</div> \
  52. <img src="' + e.target.result + '" title="' + escape(f.name) + '"/> \
  53. </div>');
  54. $("#thumbnails").append($div);
  55. f.target = $div;
  56. };
  57. })(file, idx);
  58. reader.readAsDataURL(file);
  59. }
  60. var mode = 1; //0:여러 파일을 한번에 업로드, 1:여러 파일을 각각 차례대로 업로드
  61. var uploadStatus = {
  62. total : 0,
  63. count : 0
  64. };
  65. $("#btnSubmit").on("click", function(e) {
  66. if(mode == 0)
  67. groupUpload();
  68. else if(mode == 1) {
  69. $.each(uploadFiles, function(i, file) {
  70. if(file.upload != 'disable')
  71. uploadStatus.total++;
  72. });
  73. eachUpload();
  74. }
  75. });
  76. //전체파일 한번에 업로드
  77. function groupUpload() {
  78. var formData = new FormData();
  79. $.each(uploadFiles, function(i, file) {
  80. if(file.upload != 'disable')
  81. formData.append('upload-file', file, file.name);
  82. });
  83. $.ajax({
  84. url: '/api/etc/file/upload',
  85. data : formData,
  86. type : 'post',
  87. contentType : false,
  88. processData: false,
  89. xhr: function() { //XMLHttpRequest 재정의 가능
  90. var xhr = $.ajaxSettings.xhr();
  91. xhr.upload.onprogress = function(e) { //progress 이벤트 리스너 추가
  92. var percent = e.loaded * 100 / e.total;
  93. setProgress(percent);
  94. };
  95. return xhr;
  96. },
  97. success : function(ret) {
  98. alert("완료");
  99. }
  100. });
  101. }
  102. //개별 파일 업로드
  103. function eachUpload() {
  104. var file = uploadFiles.shift();
  105. if(!file) {
  106. setTimeout(alert.bind(null, '완료'), 300);
  107. return;
  108. }
  109. if(file.upload == 'disable') {
  110. eachUpload();
  111. return;
  112. }
  113. var formData = new FormData();
  114. formData.append('upload-file', file, file.name);
  115. var $selfProgress = file.target.find("progress"); //File 객체에 저장해둔 프리뷰 DOM의 progress 요소를 찾는다.
  116. $.ajax({
  117. url: '/api/etc/file/upload',
  118. data : formData,
  119. type : 'post',
  120. contentType : false,
  121. processData: false,
  122. xhr: function() { //XMLHttpRequest 재정의 가능
  123. var xhr = $.ajaxSettings.xhr();
  124. xhr.upload.onprogress = function(e) { //progress 이벤트 리스너 추가
  125. var percent = e.loaded * 100 / e.total;
  126. $selfProgress.val(percent); //개별 파일의 프로그레스바 진행
  127. };
  128. return xhr;
  129. },
  130. success : function(ret) {
  131. uploadStatus.count++;
  132. setProgress(uploadStatus.count / uploadStatus.total * 100); //전체 프로그레스바 진행
  133. setTimeout(eachUpload, 500); //다음 파일 업로드
  134. }
  135. });
  136. }
  137. var $progressBar = $("#progressBar");
  138. function setProgress(per) {
  139. $progressBar.val(per);
  140. }
  141. $("#thumbnails").on("click", ".close", function(e) {
  142. var $target = $(e.target);
  143. var idx = $target.attr('data-idx');
  144. uploadFiles[idx].upload = 'disable';
  145. $target.parent().remove();
  146. });
  147. </script>
  148. </body>
  149. </html>