Element UI Upload 컴포넌트 재업로드 문제 해결

Element UI의 el-upload 컴포넌트를 수동 업로드 모드로 사용할 때, 첫 번째 업로드 실패 이후 동일 파일을 다시 업로드할 수 없는 현상이 발생한다. 특히 전체 화면에 로딩 오버레이가 적용된 경우 사용자 경험 측면에서 치명적이다.

문제 원인 분석

업로드 컴포넌트는 내부적으로 파일마다 status 속성을 관리한다. 파일 선택 시 ready 상태로 시작하여 업로드 진행에 따라 uploading, success, fail로 전환된다.

submit() 메서드는 uploadFiles 배열에서 status === 'ready'인 항목만 필터링하여 실제 전송을 수행한다.

// element-ui/packages/upload/index.vue
submit() {
  this.uploadFiles
    .filter(file => file.status === 'ready')
    .forEach(file => {
      this.$refs['upload-inner'].upload(file.raw);
    });
}

따라서 업로드 실패 후 statusfail로 고정되면, 동일 파일을 재전송하려 해도 필터 조건에 미치지 못해 아무런 동작이 일어나지 않는다.

해결 방안 1: 상태 초기화 후 재등록

기존 파일 정보를 보존하면서 상태만 초기화하여 업로드 파이프라인을 재가동하는 방식이다.

<template>
  <el-upload
    ref="uploader"
    :limit="1"
    accept=".xlsx"
    :action="uploadEndpoint"
    :headers="authHeaders"
    :auto-upload="false"
    :on-success="onUploadSuccess"
    :on-error="onUploadError"
    :on-remove="onFileRemoved"
  >
    <el-button icon="el-icon-folder-opened" size="small">
      파일 선택
    </el-button>
  </el-upload>

  <el-button
    type="primary"
    icon="el-icon-upload"
    size="small"
    :loading="isTransmitting"
    @click="triggerUpload"
  >
    업로드
  </el-button>
</template>
<script>
export default {
  data() {
    return {
      isTransmitting: false,
      authHeaders: { Authorization: `Bearer ${fetchToken()}` },
      uploadEndpoint: `${import.meta.env.VITE_API_BASE}/api/import/excel`
    };
  },

  methods: {
    triggerUpload() {
      const instance = this.$refs.uploader;
      const target = instance.uploadFiles[0];

      if (!target) {
        this.$message.warning('업로드할 파일을 먼저 선택하세요');
        return;
      }

      this.isTransmitting = true;

      if (target.status === 'ready') {
        // 최초 업로드
        instance.submit();
      } else {
        // 실패/성공 상태의 파일을 재업로드
        const raw = target.raw;
        instance.clearFiles();
        instance.handleStart(raw);
        instance.submit();
      }
    },

    onUploadSuccess(response, uploadedFile) {
      this.isTransmitting = false;
      
      if (response.code === 200) {
        this.$message.success('파일 업로드가 완료되었습니다');
      } else {
        uploadedFile.status = 'fail';
        this.$message.error(response.message || '서버 처리 중 오류가 발생했습니다');
      }
    },

    onUploadError() {
      this.isTransmitting = false;
      this.$message.error('네트워크 오류로 업로드에 실패했습니다');
    },

    onFileRemoved() {
      this.isTransmitting = false;
    }
  }
};
</script>

해결 방안 2: 커스텀 업로드 로직 구현

컴포넌트 내부 메커니즘에 의존하지 않고, http-request 속성을 활용해 직접 업로드 로직을 제어하는 방법도 있다. 이 경우 상태 관리가 훨씬 명확해진다.

<template>
  <el-upload
    ref="customUploader"
    :limit="1"
    accept=".xlsx"
    action="#"
    :http-request="executeCustomUpload"
    :on-exceed="handleLimitExceeded"
    :before-upload="validateBeforeUpload"
  >
    <el-button icon="el-icon-document" size="small">파일 선택</el-button>
  </el-upload>
</template>
import axios from 'axios';

export default {
  data() {
    return {
      pendingRequest: null
    };
  },

  methods: {
    validateBeforeUpload(raw) {
      const MAX_SIZE = 10 * 1024 * 1024; // 10MB
      const ALLOWED_EXT = ['.xlsx'];

      const extension = raw.name.slice(raw.name.lastIndexOf('.')).toLowerCase();
      
      if (!ALLOWED_EXT.includes(extension)) {
        this.$message.error('xlsx 형식만 업로드 가능합니다');
        return false;
      }
      
      if (raw.size > MAX_SIZE) {
        this.$message.error('파일 크기는 10MB를 초과할 수 없습니다');
        return false;
      }
      
      return true;
    },

    async executeCustomUpload({ file, onProgress, onSuccess, onError }) {
      const payload = new FormData();
      payload.append('spreadsheet', file);

      try {
        this.pendingRequest = axios.CancelToken.source();
        
        const result = await axios.post(
          '/api/spreadsheet/import',
          payload,
          {
            headers: { 'Content-Type': 'multipart/form-data' },
            cancelToken: this.pendingRequest.token,
            onUploadProgress: (evt) => {
              const percent = Math.round((evt.loaded * 100) / evt.total);
              onProgress({ percent });
            }
          }
        );

        onSuccess(result.data);
        this.$message.success('업로드가 성공적으로 완료되었습니다');
      } catch (err) {
        if (axios.isCancel(err)) {
          this.$message.info('업로드가 취소되었습니다');
        } else {
          onError(err);
          this.$message.error(`업로드 실패: ${err.message}`);
        }
      } finally {
        this.pendingRequest = null;
      }
    },

    handleLimitExceeded() {
      this.$message.warning('한 번에 하나의 파일만 업로드할 수 있습니다');
    },

    abortOngoingUpload() {
      this.pendingRequest?.cancel('사용자가 업로드를 취소했습니다');
    }
  }
};

주의사항

  • handleStart는 내부 메서드이므로 Element UI 버전 업그레이드 시 동작이 달라질 수 있다. 장기적으로는 http-request 방식을 권장한다.
  • clearFiles() 호출 시 파일 목록 UI가 깜빡일 수 있으므로, 사용자 경험을 고려해 적절한 피드백을 제공해야 한다.
  • 대용량 파일 업로드 시 AbortController 또는 CancelToken을 통한 취소 메커니즘을 반드시 구현하자.

태그: Element UI Vue.js el-upload File Upload frontend

6월 4일 19:38에 게시됨