본문 바로가기
Study

[Vue.js] Excel 파일 업로드 및 다운로드 하기 (feat. 다른 이름으로 저장하기)

by 안자두 2024. 2. 21.

1. 계기

 

회사에서 진행 중인 프로젝트에서 여러 명의 사용자를 추가할 때, 엑셀 파일을 업로드한 후, 해당 파일 데이터를 파싱하여 사용하려고 하는 로직이 추가되었다.
프론트에서도 추가적으로 양식을 다운로드한 후, 작성된 파일을 다시 업로드하는 컴포넌트를 만들게 되었다.

프론트 쪽 로직은 크게 
1. 서버로부터 받은 파일 다운로드
2. 파일 서버로 업로드
두 가지다.

아직 서버 쪽 API 개발이 완성되지 않았다고 하여, 임시로 express.js로 node.js 서버를 구현해 테스트해보았다.

 


2. 개발 환경

 

현재 vue2를 사용 중이며 서버로 파일을 전송할 때에는 axios로 FormData를 사용하여 파일을 전송할 예정이다.

기본적인 UI는 아래와 같이 세팅하였다.

// App.vue
<template>
	<div class="form-wrapper">
		<button class="download-button">양식 다운로드</button>
		<div class="upload-wrapper">
			<label id="upload">파일 선택</label>
			<input for="upload" type="file" />
			<button class="upload-button">파일 업로드</button>
		</div>
	</div>
</template>

<script>
export default {
	name: 'UploadDownloadApp',
};
</script>

<style>
.form-wrapper {
	display: flex;
	flex-direction: column;
	gap: 8px;
}

.upload-button,
.download-button {
	width: 150px;
}

.upload-wrapper {
	display: flex;
	gap: 8px;
}
</style>

 

기본 구성 UI

꼭 필요한 (1)양식 다운로드 버튼, (2)파일 선택 입력창, (3)파일 업로드 버튼 세 가지만 두었다.

 

 


3. 양식 다운로드 하기

 

기본 스타일이 적용된 엑셀 파일을 서버에서 보내주면 프론트에서 해당 파일을 다운로드할 수 있도록 한다.

다운로드할 때에는 경로 선택과 다른 이름으로 저장을 할 수 있도록 해준다.

// template
<button @click="saveExcel">양식 다운</button>

// script
<script>
// 서버로부터 파일 받는 api
async getExcelApi() {
			const response = await axios.get('http://localhost:4000/excel', {
				responseType: 'blob',
			});

			return response;
},
// 다른 이름으로 저장 함수
async saveExcel() {
		try {
			const opts = {
				suggestedName: '사용자 추가 양식',
				types: [
					{
						description: 'excel file',
						accept: {
							'application/vnd.ms-excel': ['.xlsx', '.xls'],
						},
					},
				],
			};
			const file = await this.getExcelApi();

			const handle = await window.showSaveFilePicker(opts);
			const writable = await handle.createWritable();
			await writable.write(file.data);
			await writable.close();
		} catch (e) {}
},
</script>

 

window.showSaveFilePicker()를 사용하여 다른 이름으로 저장을 할 수 있도록 탐색기를 열어주었다.

 

suggestedName은 탐색기가 열렸을 때, 파일의 이름을 기본적으로 제시해 주는 옵션이다. 확장자는 자동으로 붙기 때문에 따로 작성하지 않아도 되었다.
types의 accept에 선택할 수 있는 파일의 확장자를 특정 지을 수 있는데, 나의 경우에는 엑셀 파일(xls, xlsx)만 선택할 수 있도록 제한하였다.
제한할 수 있는 확장자는 content-type을 key로 갖는 배열 형태로 나열할 수 있다.

서버에서 blob 형식으로 보내주므로 responseType을 blob으로 명시해줘야 한다.
서버로부터 받은 파일(const file = await this.getExcelApi())을 window.showSaveFilePicker()를 사용하여 추가해 주었다.
createWritable()로 받은 파일을 다른 이름으로 저장하기 위해 생성해주어야 한다.

이때 중요한 점은 writable.close()로 생성해 준 핸들을 다시 닫아야 한다는 점이다.
close()를 하지 않는다면 아래 사진처럼 파일 생성이 제대로 끝나지 않은 채 다운로드되어, 열 수 없는 파일이 된다.

ㅠㅠ

 

이렇게 구현 후, 테스트를 해보면 아래처럼 탐색기 창이 떠, 이름과 저장 위치를 선택할 수 있다. 

다운 완료!

저장하면 위처럼 선택한 폴더에 잘 저장되고, 파일을 열어보면 보내준 파일이 원본 그대로 열리는 것을 볼 수 있다.

 


4. 파일 작성 후 업로드 하기

 

이제 양식을 받았으니, 작성 후 파일을 업로드하는 로직이 필요하다.

파일의 경우, 받은 형태 그대로 서버에 보낼 수 없기 때문에 FormData로 감싼 후, 보내주었다.

코드를 나눠서 설명해 보자면,

<template>
<input
	for="upload"
	type="file"
	accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
	@change="onChange"
/>
</template>

<script>
export default {
	data() {
		return {
			uploadData: null,
		};
	},

	methods: {
		onChange(event) {
			const value = event.target.files[0];
			this.uploadData = value;
		},
        },
}
</script>

먼저 파일을 선택할 수 있는 입력창(input)이 필요하다.

input의 type을 file로 선택하면 위의 이미지처럼 파일을 선택할 수 있는 탐색기가 뜬다.
accept는 파일 종류를 제한해 준다. 나는 엑셀만 전송할 예정이라 엑셀 관련(확장자가 .xlsx, .xls) content-type만 작성해 주었다.
다른 종류의 content-type은 여기에서 확인할 수 있다.


그리고 onChange 함수를 연결해 input이 변경될 때마다 해당 값을 로컬 변수에 저장해 주었다.
이 로컬 변수, uploadData는 onChange로부터 이벤트 타깃을 확인해 파일을 저장하는 변수이다. 파일을 선택하면 event.target에 파일 정보들이 배열형태로 저장되는데, 나는 파일 선택을 하나로 제한하여 전체 배열을 저장하는 것이 아닌, 가장 첫 번째 파일만 저장하도록 하였다.

만약 파일을 하나가 아닌 여러 개 선택할 수 있도록 하려면 input 요소에 multiple 속성을 추가해 주면 된다.

 

<template>
	<button class="upload-button" @click="upload">파일 업로드</button>
</template>

<script>
export default {
	data() {
		return {
			uploadData: null,
		};
	},

	methods: {
    		async postExcelApi(data) {
			const response = await axios.post('http://localhost:4000/excel', data);
			return response;
		},
    
		async upload() {
			if (!this.uploadData) {
				alert('파일이 존재하지 않습니다. 업로드할 파일을 선택해 주세요.');
				return;
			}

			const formData = new FormData();
			formData.append('excelFile', this.uploadData);

			const response = await this.postExcelApi(formData);

			if (response.status === 200) alert('파일이 성공적으로 전송되었습니다.');
			else alert('파일 전송이 실패하였습니다.');
		},
        }
}
</script>

먼저 postExcelApi 함수를 보면, axios를 사용해 서버로 post 요청을 해주었다. 
body에는 매개변수로 받은 파일이 들어간 data를 넣어주었다.
다른 포스팅을 보면 header에 content-type을 multipart/form-data로 설정한 후, 전송해 주는데
axios에서 content-type을 자동으로 감지 후, 헤더를 추가해 주기 때문에 저렇게 데이터만 전송해 주면 된다.
https://github.com/axios/axios/issues/4406#issuecomment-1295834004

 

Can't post FormData since Axios 0.25.0 · Issue #4406 · axios/axios

Describe the issue I can't post FormData since "axios": "^0.25.0", Error: Request failed with status code 400 Example Code export const client = axios.create({ baseURL: URL_API, withCredentials: tr...

github.com

 

이 api는 업로드 버튼에 연결된 upload 함수에서 호출된다.
이 버튼은 파일을 서버로 전송하는 로직의 일부기 때문에 먼저 파일의 유무를 확인할 필요가 있다.
해서, uploadData를 확인하여 분기처리 로직을 추가해 주었다.

그리고 파일이 존재한다면, 이 파일을 FormData에 추가하여 전송할 수 있는 형태로 가공한다.
FormData는 Web api로 따로 모듈 설치 없이 사용할 수 있다.
new 키워드로 생성한 FormData인 formData에 "excelFile"이라는 key로 파일을 추가해 주었다. 그다음, 미리 만들어 둔 postExcelApi의 매개변수로 이 formData를 던져주어 서버로 api를 요청하였다.

서버에서 받은 status로 alert을 분기처리 해주면 끝!

 

파일 전송의 전체 코드는 아래처럼 된다.

// App.vue
<template>
	<div class="form-wrapper">
		<div class="upload-wrapper">
			<label id="upload">파일 선택</label>
			<input
				for="upload"
				type="file"
				accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
				@change="onChange"
			/>
			<button class="upload-button" @click="upload">파일 업로드</button>
		</div>
	</div>
</template>

<script>
import axios from 'axios';

export default {
	name: 'UploadDownloadApp',

	data() {
		return {
			uploadData: null,
		};
	},

	methods: {
    	// 파일 선택 시 값을 변수에 저장
		onChange(event) {
			const value = event.target.files[0];
			this.uploadData = value;
		},
        
        // 서버로 파일 보내는 api
		async postExcelApi(data) {
			const response = await axios.post('http://localhost:4000/excel', data);
			return response;
		},
        
        // 파일을 FormData로 감싸 서버로 보내는 로직
		async upload() {
			// 파일이 없을 때의 예외 처리
			if (!this.uploadData) {
				alert('파일이 존재하지 않습니다. 업로드할 파일을 선택해 주세요.');
				return;
			}

			// FormData에 파일 추가
			const formData = new FormData();
			formData.append('formData', this.uploadData);

			// server로 파일 전송
			const response = await this.postExcelApi(formData);

			// 성공 여부에 따른 분기 alert
			if (response.status === 200) alert('파일이 성공적으로 전송되었습니다.');
			else alert('파일 전송이 실패하였습니다.');
		},
	},
};
</script>

 


REF

https://github.com/axios/axios/issues/4406#issuecomment-1295834004

https://developer.mozilla.org/ko/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types

728x90