보통 Drag & Drop이 가능한 파일 업로드 기능을 구현할 때는 기존에 있던 라이브러리를 쓰면 간단하고 빠르게 해결할 수 있다.
지금까지 제일 유명한 Dropzone.js나 filepond.js 등을 사용해봤는데, 이번에는 라이브러리 없이 간단하게 파일 업로드를 구현해봤다.
어려울 줄 알았는데 생각보다 쉬워서 놀랐다.
<template>
<div class="container">
<div class="file-upload-container"
@dragenter="onDragenter"
@dragover="onDragover"
@dragleave="onDragleave"
@drop="onDrop"
@click="onClick"
>
<div class="file-upload" :class="isDragged ? 'dragged' : ''">
Drag & Drop Files
</div>
</div>
<!-- 파일 업로드 -->
<input type="file" ref="fileInput" class="file-upload-input" @change="onFileChange" multiple>
<!-- 업로드된 리스트 -->
<div class="file-upload-list">
<div class="file-upload-list__item" v-for="(file, index) in fileList" :key="index">
<div class="file-upload-list__item__data">
<img class="file-upload-list__item__data-thumbnail" :src="file.src">
<div class="file-upload-list__item__data-name">
{{ file.name }}
</div>
</div>
<div class="file-upload-list__item__btn-remove" @click="handleRemove(index)">
삭제
</div>
</div>
</div>
</div>
</template>
<style lang="scss">
.container {
min-height: 300px;
width: 500px;
margin: 0 auto;
}
.file-upload {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
border: transparent;
border-radius: 20px;
cursor: pointer;
&.dragged {
border: 1px dashed powderblue;
opacity: .6;
}
&-container {
height: 300px;
padding: 20px;
margin: 0 auto;
box-shadow: 0 0.625rem 1.25rem #0000001a;
border-radius: 20px;
}
&-input {
display: none;
}
&-list {
margin-top: 10px;
width: 100%;
&__item {
padding: 10px;
display: flex;
justify-content: space-between;
align-items: center;
&__data {
display: flex;
align-items: center;
&-thumbnail {
margin-right: 10px;
border-radius: 20px;
width: 120px;
height: 120px;
}
}
&__btn-remove {
cursor: pointer;
border: 1px solid powderblue;
display: flex;
justify-content: center;
align-items: center;
padding: 5px;
border-radius: 6px;
}
}
}
}
</style>
CSS는 특별할 게 없다.
Drag & Drop 영역에 파일이 드래그 될 때 해당 영역에 보더와 opactity 효과를 줬다.
<script>
onClick () {
this.$refs.fileInput.click()
},
onDragenter (event) {
// class 넣기
this.isDragged = true
},
onDragleave (event) {
// class 삭제
this.isDragged = false
},
onDragover (event) {
// 드롭을 허용하도록 prevetDefault() 호출
event.preventDefault()
},
onDrop (event) {
// 기본 액션을 막음 (링크 열기같은 것들)
event.preventDefault()
this.isDragged = false
const files = event.dataTransfer.files
this.addFiles(files)
},
onFileChange (event) {
const files = event.target.files
this.addFiles(files)
}
</script>
우선 Drag & Drop 영역을 클릭 했을 때, input type=file을 클릭한 것과 같은 효과를 낼 수 있는 이벤트를 추가한다.
dragenter, 즉 Drag & Drop 영역 안으로 파일이 드래그 되는 경우 isDragged 변수의 상태 값을 변경해 class 가 추가되도록 한다.
dragleave, 즉 Drag & Drop 영역 바깥으로 파일이 나가는 경우 isDragged 변수의 상태값을 다시 변경해 추가되었던 class를 삭제해준다.
dragover 이벤트에서 preventDefault()를 통해 드롭이 가능하도록 허용한다.
drop 이벤트에서 기본 액션을 막기 위해 preventDefault()를 실행하고, event.dataTransfer.files 를 통해 전달된 파일 객체를 addFiles 메소드에 넘긴다.
onFileChange는 input type=file 의 change 이벤트에 걸리는 메소드인데, 파라미터로 넘어온 파일 객체를 addFiles 메소드로 넘긴다.
<script>
async addFiles (files) {
for(let i = 0; i < files.length; i++) {
const src = await this.readFiles(files[i])
files[i].src = src
this.fileList.push(files[i])
}
},
// FileReader를 통해 파일을 읽어 thumbnail 영역의 src 값으로 셋팅
async readFiles (files) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = async (e) => {
resolve(e.target.result)
}
reader.readAsDataURL(files)
})
},
handleRemove (index) {
this.fileList.splice(index, 1)
}
</script>
정말 시간이 없고 여러 기능이 필요하다 싶을 땐 라이브러리를 사용하는 게 좋을 것 같고, 딱히 큰 기능이 필요 없는 경우엔 이번에 만든 걸 커스텀해서 사용하면 좋을 것 같다.