바닐라 JavaScript로 드래그 앤 드롭 파일 업로더를 만드는 방법
게시 됨: 2022-03-10파일 선택 입력은 개발자가 원하는 방식으로 스타일을 지정하기 어렵기 때문에 많은 사람들이 단순히 숨기고 대신 파일 선택 대화 상자를 여는 버튼을 생성한다는 것은 알려진 사실입니다. 하지만 요즘에는 파일 선택을 처리하는 더 멋진 방법인 끌어서 놓기가 있습니다.
기술적으로 이것은 파일 선택 입력의 대부분( 전부 는 아닐지라도) 구현에서 파일을 끌어 선택하도록 허용했기 때문에 이미 가능했지만 실제로 file
요소를 표시해야 합니다. 따라서 실제로 브라우저에서 제공한 API를 사용하여 끌어서 놓기 파일 선택기 및 업로더를 구현해 보겠습니다.
이 기사에서는 "바닐라" ES2015+ JavaScript(프레임워크 또는 라이브러리 없음)를 사용하여 이 프로젝트를 완료할 것이며 브라우저에서 JavaScript에 대한 실무 지식이 있다고 가정합니다. 이 예제는 ES5 구문으로 쉽게 변경되거나 Babel에 의해 변환될 수 있는 ES2015+ 구문을 제외하고 모든 에버그린 브라우저와 IE 10 및 11과 호환되어야 합니다.
만들 내용을 간단히 살펴보겠습니다.
드래그 앤 드롭 이벤트
가장 먼저 논의해야 할 것은 드래그 앤 드롭과 관련된 이벤트입니다. 이 이벤트는 이 기능의 원동력이기 때문입니다. 드래그 앤 드롭과 관련하여 브라우저가 실행하는 이벤트는 모두 8가지입니다. drag
, dragend
, dragenter
, dragexit
, dragleave
, dragover
, dragstart
및 drop
. drag
, dragend
, dragexit
및 dragstart
는 모두 드래그되는 요소에서 실행되기 때문에 모든 항목을 살펴보지는 않을 것입니다. 우리의 경우 DOM 요소가 아닌 파일 시스템에서 파일을 드래그할 것입니다. , 따라서 이러한 이벤트는 팝업되지 않습니다.
이에 대해 궁금하다면 MDN에서 이러한 이벤트에 대한 문서를 읽을 수 있습니다.
예상대로 대부분의 브라우저 이벤트에 대해 이벤트 핸들러를 등록하는 것과 동일한 방식으로 이러한 이벤트에 대한 이벤트 addEventListener
를 등록할 수 있습니다.
let dropArea = document.getElementById('drop-area') dropArea.addEventListener('dragenter', handlerFunction, false) dropArea.addEventListener('dragleave', handlerFunction, false) dropArea.addEventListener('dragover', handlerFunction, false) dropArea.addEventListener('drop', handlerFunction, false)
다음은 언어를 더 명확하게 하기 위해 코드 샘플에서 dropArea
를 사용하여 이러한 이벤트가 수행하는 작업을 설명하는 작은 표입니다.
이벤트 | 언제 발사되나요? |
---|---|
dragenter | 드래그한 항목을 dropArea 위로 드래그하여 사용자가 드롭 영역에 드롭하면 드롭 이벤트의 대상이 됩니다. |
dragleave | 드래그한 항목은 dropArea에서 다른 요소로 드래그되어 대신 drop 이벤트의 대상이 됩니다. |
dragover | 드래그한 항목이 dropArea 위에 있고 이동하는 동안 수백 밀리초마다. |
drop | 사용자가 마우스 버튼을 놓고 드래그한 항목을 dropArea에 놓습니다. |
드래그한 항목을 dropArea의 자식 위로 드래그하면 dropArea
에서 dragleave
가 dropArea
되고 새 target
이기 때문에 dragenter
가 해당 자식 요소에서 실행됩니다. drop
이벤트는 dropArea
까지 전파되므로(다른 이벤트 리스너가 도달하기 전에 전파를 중지하지 않는 한) 이벤트 target
이 아님에도 dropArea
에서 계속 발생합니다.
또한 사용자 지정 드래그 앤 드롭 상호 작용을 생성하려면 이러한 이벤트에 대한 각 리스너에서 event.preventDefault()
를 호출해야 합니다. 그렇지 않으면 브라우저는 drop
이벤트 핸들러로 파일을 보내는 대신 드롭한 파일을 열게 됩니다.
양식 설정
끌어서 놓기 기능을 추가하기 전에 표준 file
입력이 있는 기본 양식이 필요합니다. 기술적으로 이것은 필요하지 않지만 사용자가 끌어서 놓기 API를 지원하지 않는 브라우저를 사용하는 경우 대안으로 제공하는 것이 좋습니다.
<div> <form class="my-form"> <p>Upload multiple files with the file dialog or by dragging and dropping images onto the dashed region</p> <input type="file" multiple accept="image/*" onchange="handleFiles(this.files)"> <label class="button" for="fileElem">Select some files</label> </form> </div>
꽤 단순한 구조. input
에서 onchange
핸들러를 볼 수 있습니다. 나중에 살펴보겠습니다. form
에 action
을 추가하고 JavaScript를 활성화하지 않은 사람들을 돕기 위해 submit
버튼을 추가하는 것도 좋은 생각입니다. 그런 다음 JavaScript를 사용하여 더 깨끗한 형태로 제거할 수 있습니다. 어떤 경우든 사내에서 개발한 것이든 Cloudinary와 같은 서비스를 사용하여 업로드하든 상관없이 업로드를 수락하려면 서버 측 스크립트 가 필요합니다. 이 메모 외에는 특별한 것이 없으므로 다음과 같이 몇 가지 스타일을 적용해 보겠습니다.
#drop-area { border: 2px dashed #ccc; border-radius: 20px; width: 480px; font-family: sans-serif; margin: 100px auto; padding: 20px; } #drop-area.highlight { border-color: purple; } p { margin-top: 0; } .my-form { margin-bottom: 10px; } #gallery { margin-top: 10px; } #gallery img { width: 150px; margin-bottom: 10px; margin-right: 10px; vertical-align: middle; } .button { display: inline-block; padding: 10px; background: #ccc; cursor: pointer; border-radius: 5px; border: 1px solid #ccc; } .button:hover { background: #ddd; } #fileElem { display: none; }
이러한 스타일 중 많은 부분이 아직 작동하지 않지만 괜찮습니다. 현재 하이라이트는 file
입력이 숨겨져 있지만 label
이 버튼처럼 보이도록 스타일이 지정되어 사람들이 클릭하여 파일 선택 대화 상자를 불러올 수 있다는 것을 알게 된다는 것입니다. 또한 드롭 영역을 점선으로 표시하여 규칙을 따르고 있습니다.
끌어서 놓기 기능 추가
이제 상황의 핵심인 드래그 앤 드롭으로 이동합니다. 페이지 하단이나 별도의 파일에 스크립트를 삽입해 보겠습니다. 스크립트에서 가장 먼저 필요한 것은 드롭 영역에 대한 참조이므로 여기에 몇 가지 이벤트를 첨부할 수 있습니다.
let dropArea = document.getElementById('drop-area')
이제 몇 가지 이벤트를 추가해 보겠습니다. 기본 동작을 방지하고 이벤트가 필요 이상으로 버블링되는 것을 방지하기 위해 모든 이벤트에 핸들러를 추가하는 것으로 시작하겠습니다.
;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { dropArea.addEventListener(eventName, preventDefaults, false) }) function preventDefaults (e) { e.preventDefault() e.stopPropagation() }
이제 CSS를 사용하여 놓기 영역의 테두리 색상을 변경하여 항목을 올바른 영역 위로 드래그했음을 사용자에게 알리는 표시기를 추가해 보겠습니다. 스타일은 이미 #drop-area.highlight
선택기 아래에 있어야 하므로 JS를 사용하여 필요할 때 해당 highlight
클래스를 추가 및 제거하겠습니다.
;['dragenter', 'dragover'].forEach(eventName => { dropArea.addEventListener(eventName, highlight, false) }) ;['dragleave', 'drop'].forEach(eventName => { dropArea.addEventListener(eventName, unhighlight, false) }) function highlight(e) { dropArea.classList.add('highlight') } function unhighlight(e) { dropArea.classList.remove('highlight') }
앞서 언급한 내용 때문에 강조 표시를 위해 dragenter
와 dragover
를 모두 사용해야 했습니다. dropArea
바로 위에서 마우스를 가져간 다음 해당 자식 중 하나 위로 마우스를 가져가면 dragleave
가 실행되고 강조 표시가 제거됩니다. dragover
이벤트는 dragenter
및 dragleave
이벤트 후에 시작되므로 강조 표시는 제거되는 것을 보기 전에 dropArea
에 다시 추가됩니다.
또한 드래그한 항목이 지정된 영역을 벗어나거나 항목을 놓을 때 강조 표시를 제거합니다.
이제 우리가 해야 할 일은 일부 파일이 삭제되었을 때 수행할 작업을 파악하는 것입니다.
dropArea.addEventListener('drop', handleDrop, false) function handleDrop(e) { let dt = e.dataTransfer let files = dt.files handleFiles(files) }
이것은 우리를 거의 완료로 가져오지는 않지만 두 가지 중요한 일을 합니다.
- 삭제된 파일에 대한 데이터를 가져오는 방법을 보여줍니다.
-
onchange
핸들러와 함께file
input
이 있었던 동일한 위치로 우리를 가져handleFiles
.
files
은 배열이 아니라 FileList
입니다. 그래서, 우리가 handleFiles
를 구현할 때 그것을 더 쉽게 반복하기 위해 그것을 배열로 변환할 필요가 있을 것입니다:
function handleFiles(files) { ([...files]).forEach(uploadFile) }
그것은 반항적이었습니다. 실제 고기를 위해 uploadFile
로 들어가 보겠습니다.
function uploadFile(file) { let url = 'YOUR URL HERE' let formData = new FormData() formData.append('file', file) fetch(url, { method: 'POST', body: formData }) .then(() => { /* Done. Inform the user */ }) .catch(() => { /* Error. Inform the user */ }) }
여기서 우리는 서버에 보낼 양식 데이터를 생성하기 위한 내장 브라우저 API인 FormData
를 사용합니다. 그런 다음 fetch
API를 사용하여 실제로 이미지를 서버로 보냅니다. 백엔드 또는 서비스와 작동하도록 URL을 변경하고 서버에 필요한 모든 정보를 제공하는 데 필요할 수 있는 추가 양식 데이터를 formData.append
로 변경해야 합니다. 또는 Internet Explorer를 지원하려면 XMLHttpRequest
를 사용할 수 있습니다. 즉, uploadFile
이 대신 다음과 같이 표시됩니다.
function uploadFile(file) { var url = 'YOUR URL HERE' var xhr = new XMLHttpRequest() var formData = new FormData() xhr.open('POST', url, true) xhr.addEventListener('readystatechange', function(e) { if (xhr.readyState == 4 && xhr.status == 200) { // Done. Inform the user } else if (xhr.readyState == 4 && xhr.status != 200) { // Error. Inform the user } }) formData.append('file', file) xhr.send(formData) }
서버 설정 방법에 따라 200
이 아닌 다른 범위의 status
번호를 확인하고 싶을 수도 있지만 우리의 목적에는 이것이 작동합니다.
추가 기능
이것이 기본 기능의 전부이지만 종종 더 많은 기능을 원합니다. 특히 이 자습서에서는 선택한 모든 이미지를 사용자에게 표시하는 미리 보기 창을 추가한 다음 사용자가 업로드 진행률을 볼 수 있도록 하는 진행률 표시줄을 추가합니다. 이제 이미지 미리보기를 시작해 보겠습니다.
이미지 미리보기
이를 수행할 수 있는 몇 가지 방법이 있습니다. 이미지가 업로드될 때까지 기다렸다가 서버에 이미지의 URL을 보내도록 요청할 수 있지만, 이는 기다려야 하고 때때로 이미지가 꽤 클 수 있음을 의미합니다. 오늘 살펴볼 대안은 drop
이벤트에서 받은 파일 데이터에 FileReader API를 사용하는 것입니다. 이것은 비동기식이며 대안적으로 FileReaderSync를 사용할 수 있지만 여러 개의 큰 파일을 연속으로 읽으려고 할 수 있으므로 스레드를 꽤 오랫동안 차단하고 실제로 경험을 망칠 수 있습니다. 이제 previewFile
함수를 만들고 작동 방식을 살펴보겠습니다.
function previewFile(file) { let reader = new FileReader() reader.readAsDataURL(file) reader.onloadend = function() { let img = document.createElement('img') img.src = reader.result document.getElementById('gallery').appendChild(img) } }
여기에서 new FileReader
를 만들고 File
객체를 사용하여 readAsDataURL
을 호출합니다. 언급했듯이 이것은 비동기식이므로 읽기 결과를 얻으려면 onloadend
이벤트 핸들러를 추가해야 합니다. 그런 다음 기본 64 데이터 URL을 새 이미지 요소의 src
로 사용하고 이를 gallery
요소에 추가합니다. 이제 이 작업을 수행하기 위해 수행해야 하는 작업은 두 가지뿐입니다. gallery
요소를 추가하고 previewFile
이 실제로 호출되는지 확인합니다.
먼저 form
태그의 끝 바로 뒤에 다음 HTML을 추가합니다.
<div></div>
특별한 것은 없습니다. 그것은 단지 div입니다. 스타일과 그 안의 이미지는 이미 지정되어 있으므로 할 일이 없습니다. 이제 handleFiles
함수를 다음과 같이 변경해 보겠습니다.
function handleFiles(files) { files = [...files] files.forEach(uploadFile) files.forEach(previewFile) }
구성과 같은 몇 가지 방법이 있습니다. 또는 그 previewFile
uploadFile
실행한 forEach
에 대한 단일 콜백도 있지만 이 방법도 작동합니다. 이를 통해 일부 이미지를 드롭하거나 선택하면 양식 아래에 거의 즉시 표시되어야 합니다. 이것에 대한 흥미로운 점은 특정 응용 프로그램에서 실제로 이미지를 업로드하고 싶지 않고 대신 해당 데이터 URL을 나중에 앱에서 액세스할 수 있도록 localStorage
또는 기타 클라이언트 측 캐시에 저장할 수 있다는 것입니다. 나는 개인적으로 이것에 대한 좋은 사용 사례를 생각할 수 없지만 몇 가지가 있을 의향이 있습니다.
진행 상황 추적
시간이 걸릴 수 있는 경우 진행률 표시줄을 통해 사용자가 진행 상황이 실제로 진행되고 있음을 깨닫고 완료하는 데 걸리는 시간을 알 수 있습니다. HTML5 progress
태그 덕분에 진행률 표시기를 추가하는 것은 매우 쉽습니다. 이번에는 HTML 코드에 추가하는 것으로 시작하겠습니다.
<progress max=100 value=0></progress>
label
바로 뒤 또는 form
과 갤러리 div
사이에 원하는 위치에 배치할 수 있습니다. 이를 위해 body
태그 내에서 원하는 위치에 배치할 수 있습니다. 이 예제에는 스타일이 추가되지 않았으므로 서비스 가능한 브라우저의 기본 구현이 표시됩니다. 이제 JavaScript를 추가하는 작업을 해보자. 먼저 fetch
를 사용한 구현을 살펴본 다음 XMLHttpRequest
버전을 보여줍니다. 시작하려면 스크립트 상단에 몇 가지 새로운 변수가 필요합니다.
let filesDone = 0 let filesToDo = 0 let progressBar = document.getElementById('progress-bar')
fetch
를 사용할 때 업로드가 완료된 시간만 결정할 수 있으므로 업로드하기 위해 선택한 파일 수( filesToDo
로)와 업로드가 완료된 파일 수( filesDone
로)만 추적합니다. 빠르게 업데이트할 수 있도록 #progress-bar
요소에 대한 참조도 유지하고 있습니다. 이제 진행 상황을 관리하기 위한 몇 가지 함수를 만들어 보겠습니다.
function initializeProgress(numfiles) { progressBar.value = 0 filesDone = 0 filesToDo = numfiles } function progressDone() { filesDone++ progressBar.value = filesDone / filesToDo * 100 }
업로드를 시작하면 initializeProgress
가 호출되어 진행률 표시줄을 재설정합니다. 그런 다음 업로드가 완료될 때마다 progressDone
을 호출하여 완료된 업로드 수를 늘리고 진행률 표시줄을 업데이트하여 현재 진행 상황을 표시합니다. 따라서 몇 가지 이전 함수를 업데이트하여 이러한 함수를 호출해 보겠습니다.
function handleFiles(files) { files = [...files] initializeProgress(files.length) // <- Add this line files.forEach(uploadFile) files.forEach(previewFile) } function uploadFile(file) { let url = 'YOUR URL HERE' let formData = new FormData() formData.append('file', file) fetch(url, { method: 'POST', body: formData }) .then(progressDone) // <- Add `progressDone` call here .catch(() => { /* Error. Inform the user */ }) }
그리고 그게 다야. 이제 XMLHttpRequest
구현을 살펴보자. 우리는 간단히 uploadFile
을 업데이트할 수 있지만 XMLHttpRequest
는 실제로 fetch
보다 더 많은 기능을 제공합니다. 완성 된. 이 때문에 얼마나 많은 요청이 완료되었는지가 아니라 각 요청의 완료율을 추적해야 합니다. 따라서 filesDone
및 filesToDo
에 대한 선언을 다음으로 교체하는 것으로 시작하겠습니다.
let uploadProgress = []
그런 다음 기능도 업데이트해야 합니다. progressDone
의 이름을 updateProgress
로 바꾸고 다음과 같이 변경합니다.
function initializeProgress(numFiles) { progressBar.value = 0 uploadProgress = [] for(let i = numFiles; i > 0; i--) { uploadProgress.push(0) } } function updateProgress(fileNumber, percent) { uploadProgress[fileNumber] = percent let total = uploadProgress.reduce((tot, curr) => tot + curr, 0) / uploadProgress.length progressBar.value = total }
이제 initializeProgress
는 0으로 채워진 numFiles
와 동일한 길이의 배열을 초기화하여 각 파일이 0% 완료되었음을 나타냅니다. updateProgress
에서 진행률이 업데이트된 이미지를 찾고 해당 인덱스의 값을 제공된 percent
로 변경합니다. 그런 다음 모든 백분율의 평균을 취하여 총 진행률을 계산하고 계산된 총계를 반영하도록 진행률 표시줄을 업데이트합니다. 우리는 여전히 fetch
예제에서 했던 것과 같이 handleFiles
에서 initializeProgress
를 호출하므로 이제 업데이트해야 할 것은 updateProgress
를 호출하기 위해 uploadFile
을 호출하는 것뿐입니다.
function uploadFile(file, i) { // <- Add `i` parameter var url = 'YOUR URL HERE' var xhr = new XMLHttpRequest() var formData = new FormData() xhr.open('POST', url, true) // Add following event listener xhr.upload.addEventListener("progress", function(e) { updateProgress(i, (e.loaded * 100.0 / e.total) || 100) }) xhr.addEventListener('readystatechange', function(e) { if (xhr.readyState == 4 && xhr.status == 200) { // Done. Inform the user } else if (xhr.readyState == 4 && xhr.status != 200) { // Error. Inform the user } }) formData.append('file', file) xhr.send(formData) }
가장 먼저 주목해야 할 것은 i
매개변수를 추가했다는 것입니다. 이것은 파일 목록에 있는 파일의 색인입니다. 이 매개변수를 전달하기 위해 handleFiles
를 업데이트할 필요가 없습니다. 이 매개변수는 이미 요소의 인덱스를 콜백에 대한 두 번째 매개변수로 제공하는 forEach
를 사용하고 있기 때문입니다. 또한 진행 상황과 함께 updateProgress
를 호출할 수 있도록 xhr.upload
에 progress
이벤트 리스너를 추가했습니다. 이벤트 개체(코드에서 e
라고 함)에는 두 가지 관련 정보가 있습니다. 즉 지금까지 업로드된 바이트 수가 포함 loaded
됨과 파일의 총 바이트 수가 포함된 total
입니다.
|| 100
|| 100
조각이 거기에 있습니다. 때때로 오류가 있는 경우 e.loaded
및 e.total
이 0이 되기 때문에 계산이 NaN
으로 나오므로 파일이 완료되었음을 보고하는 대신 100
이 사용됩니다. 0
을 사용할 수도 있습니다. 두 경우 모두 사용자에게 알릴 수 있도록 readystatechange
핸들러에 오류가 표시됩니다. 이것은 단지 NaN
으로 수학을 시도할 때 예외가 발생하는 것을 방지하기 위한 것입니다.
결론
마지막 조각입니다. 이제 끌어서 놓기를 통해 이미지를 업로드하고, 즉시 업로드되는 이미지를 미리 보고, 진행률 표시줄에서 업로드 진행률을 볼 수 있는 웹 페이지가 생겼습니다. CodePen에서 작동 중인 최종 버전( XMLHttpRequest
포함)을 볼 수 있지만 파일을 업로드하는 서비스에 제한이 있으므로 많은 사람들이 테스트하면 잠시 중단될 수 있습니다.