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);
});
}
따라서 업로드 실패 후 status가 fail로 고정되면, 동일 파일을 재전송하려 해도 필터 조건에 미치지 못해 아무런 동작이 일어나지 않는다.
해결 방안 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을 통한 취소 메커니즘을 반드시 구현하자.