[Vue.js] 라이브러리 없이 Drag & Drop 가능한 파일 업로드 만들기

호박고구마·2021년 10월 8일
4
post-thumbnail
post-custom-banner

보통 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>
  1. Drag & Drop 할 영역에 Drag & Drop과 Click 이벤트를 걸어준다.
  2. input type=file 태그는 화면에 보이지 않게 css 로 숨기고 Change 이벤트를 걸어준다.
  3. 업로드 된 파일 리스트를 보여줄 리스트를 만든다.

CSS

<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>
  1. 우선 Drag & Drop 영역을 클릭 했을 때, input type=file을 클릭한 것과 같은 효과를 낼 수 있는 이벤트를 추가한다.

  2. dragenter, 즉 Drag & Drop 영역 안으로 파일이 드래그 되는 경우 isDragged 변수의 상태 값을 변경해 class 가 추가되도록 한다.

  3. dragleave, 즉 Drag & Drop 영역 바깥으로 파일이 나가는 경우 isDragged 변수의 상태값을 다시 변경해 추가되었던 class를 삭제해준다.

  4. dragover 이벤트에서 preventDefault()를 통해 드롭이 가능하도록 허용한다.

  5. drop 이벤트에서 기본 액션을 막기 위해 preventDefault()를 실행하고, event.dataTransfer.files 를 통해 전달된 파일 객체를 addFiles 메소드에 넘긴다.

  6. 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>
  1. addFiles 메소드에서는 파라미터로 넘어온 파일 객체를 읽고 fileList 배열에 추가한다.
  2. readFiles 메소드는 업로드한 파일을 읽어 썸네일을 생성하기 위해 만든 메소드다.
    주의할 점은 FileReader 객체는 비동기로 파일을 읽는다는 점이다. 때문에 실제 화면에 보여줄 fileList 배열에 파일 객체를 추가하기 전에 async, await 을 통해 먼저 파일을 읽는다.
    readAsDataURL 메소드를 통해 읽은 파일의 URL 값을 얻을 수 있다. 해당 URL 값을 화면에 보여줄 파일의 src 값으로 셋팅해주면, 사진 파일의 경우 해당 src 값을 썸네일 처럼 활용할 수 있다.
  3. hadleRemove의 경우 파일 리스트에서 삭제 버튼을 클릭시 splice 메소드를 통해 해당 배열에서 index에 해당하는 인자를 삭제한다.


Drag & Drop 기능은 막연하게 어려울 거라고 생각했는데 아니었다.
생각보다 쉬워서 당황..
여기에 실제 프로젝트에서는 파일 유효성 검사(파일 개수, 용량, 확장자, mime 타입 등) 로직과 Sheet.js 라이브러리를 통해 업로드한 엑셀 파일을 json으로 가공하는 로직을 따로 추가했다.

정말 시간이 없고 여러 기능이 필요하다 싶을 땐 라이브러리를 사용하는 게 좋을 것 같고, 딱히 큰 기능이 필요 없는 경우엔 이번에 만든 걸 커스텀해서 사용하면 좋을 것 같다.

post-custom-banner

0개의 댓글