Yeppyshiba Blog

About
coding

axios와 vue를 이용한 분할 업로드

6/20/2019

devcodingchunk uploadaxiosvuelaravelphpjavascript

들어가며

파일 업로드 구현이야 많이들 해보셨겠지만 용량이 커진다면? 생각보다 신경쓸게 많아집니다.

어느 세월에 다 올리냐...
어느 세월에 다 올리냐...
분할 업로드를 구현했을 때 얻을 수 있는 이점들은 생각보다 꽤 많습니다. 가령...
  • 낮은 서버 대기 시간
  • 느린 클라이언트에게도 축복을
  • 또한 멋진 업로드 매니저도 구현이 가능하구요
오늘은 axios 라이브러리를 활용하여
resumablejs 라이브러리 메뉴얼을 참고하여 분할 업로드 기능을 구현해보도록 하겠습니다.

원리

원리는 정말 간단합니다. 파일을 그냥 짤라서 서버에 던지고, 서버에서는 파일을 합치면 됩니다.

참 쉽죠?
참 쉽죠?

물론 말은 쉽겠지만 코드로 이야기 하겠습니다.

백엔드

먼저 이 글의 중점은 프론트엔드이므로, 기존 라이브러리를 활용하도록 하겠습니다. 빠른 진행을 위해서 laravel 을 선택 했습니다.

shell
1composer create-project --prefer-dist laravel/laravel

이 라이브러리가 쓸만한거 같더군요.

shell
1composer require pion/laravel-chunk-upload

이제 업로드를 처리할 Controller 를 정의합니다. 업로드 진행시 퍼센트를 리턴해주며, 완료시에는 파일 병합 및 이동을 담당하게 됩니다.

php
1class UploadController extends Controller
2{
3 //
4 public function request(Request $request, FileReceiver $receiver)
5 {
6 if ($receiver->isUploaded() === false) {
7 throw new UploadMissingFileException();
8 }
9
10 // receive the file
11 $save = $receiver->receive();
12
13 // check if the upload has finished (in chunk mode it will send smaller files)
14 if ($save->isFinished()) {
15 // save the file and return any response you need
16 return $this->saveFile($save->getFile());
17 }
18
19 $handler = $save->handler();
20
21 return response()->json([
22 "done" => $handler->getPercentageDone(),
23 "status" => true
24 ]);
25 }
26
27 /**
28 * Saves the file
29 *
30 * @param UploadedFile $file
31 *
32 * @return \Illuminate\Http\JsonResponse
33 */
34 protected function saveFile(UploadedFile $file)
35 {
36 $fileName = $this->createFilename($file);
37
38 // Group files by the date
39 $yearFolder = date('Y');
40 $monthFolder = date('m');
41 $filePath = "upload/{$yearFolder}/{$monthFolder}/";
42 $finalPath = storage_path("app/public/{$filePath}");
43
44 // move the file name
45 $file->move($finalPath, $fileName);
46
47 return [
48 'path' => Storage::url($filePath . $fileName)
49 ];
50 }
51
52 /**
53 * Create unique filename for uploaded file
54 * @param UploadedFile $file
55 * @return string
56 */
57 protected function createFilename(UploadedFile $file)
58 {
59 return implode([
60 time(),
61 mt_rand(100, 999),
62 '.',
63 $file->getClientOriginalExtension()
64 ]);
65 }
66}

프론트엔드

라라벨이 기본셋으로 vue 를 지원하여 선택하였습니다. (물론 react 도 지원합니다.)
별다른 설정 없이도 hot reload + webpack 을 지원하여 대!단!히! 편합니다.
아 webpack 아시는구나!
아 webpack 아시는구나!

input 태그를 담는 vue component 를 만들어 봅시다.

html
1<input
2 type="file"
3 class="custom-file-input"
4 accept="video/*,audio/*,image/*"
5 ref="fileContainer"
6 @change="onChangeFile"
7/>

파일을 첨부하면, data 에 자동으로 등록되게끔 했습니다.

javascript
1 onChangeFile() {
2 const file = this.$refs.fileContainer.files;
3 this.file = file.length > 0 ? file[0] : null;
4 }

onsubmit 이벤트 시점에 axios 로 POST 호출하도록 설정하겠습니다.

javascript
1const api = axios.create({
2 headers: {
3 'Content-type': 'application/x-www-form-urlencoded',
4 Accept: 'application/json',
5 },
6});
7
8const chunkSize = 1024 * 1024;

axios 개체를 만들어주고, 한번에 1Mb 씩 업로드하도록 사이즈를 지정했습니다.

javascript
1const start = options.chunkNumber * chunkSize;
2const end = Math.min(file.size, start + chunkSize);
3
4let currentChunkSize = chunkSize;
5if (options.chunkNumber + 1 === options.blockCount) {
6 currentChunkSize = file.size - start;
7}
8
9const params = new FormData();
10params.append('resumableChunkNumber', options.chunkNumber + 1);
11params.append('resumableChunkSize', currentChunkSize);
12params.append('resumableCurrentChunkSize', currentChunkSize);
13params.append('resumableTotalSize', file.size);
14params.append('resumableType', file.type);
15params.append('resumableIdentifier', options.identifier);
16params.append('resumableFilename', file.name);
17params.append('resumableRelativePath', file.name);
18params.append('resumableTotalChunks', options.blockCount);
19params.append('file', file.slice(start, end), file.name);

resumablejs 라이브러리 메뉴얼을 참고하여 FormData 객체를 만들어줍니다.

여기에서 참고할 내용은 chunk 순서에 따라서 파일을 짜르는 것과 FormData 객체에 append 시에 세번째 파라미터를 추가하는 것입니다. 자세한 내용은 BlobFormData 레퍼런스를 참조하세요.
javascript
1return api
2 .post(endpoint, params)
3 .then((res) => {
4 options.onProgress && options.onProgress(parseInt((end / file.size) * 100, 10), res);
5 if (end === file.size) {
6 options.onSuccess && options.onSuccess(res);
7 } else {
8 options.chunkNumber++;
9 return chunkUploader(endpoint, file, options);
10 }
11 })
12 .catch((err) => {
13 options.onError && options.onError(err);
14 });

그런 다음 callback event 들을 정의해주기 위한 처리를 합니다. 최초 실행한 이후에 onProgress callback 으로는 현재 진행상태를 공유하고 업로드 완료되면 onSuccess callback 을 실행하게 됩니다.

javascript
1export default {
2 chunk: (endpoint, file, onProgress, onError, onSuccess) => {
3 const blockCount = Math.ceil(file.size / chunkSize);
4 const chunkNumber = 0;
5 const identifier = `${file.size}-${file.name.replace('.', '')}`;
6
7 return chunkUploader(endpoint, file, {
8 blockCount,
9 identifier,
10 chunkNumber,
11 onProgress,
12 onError,
13 onSuccess,
14 });
15 },
16};

최초 업로드 요청을 처리하기 위한 함수를 정의합니다. identifier 는 병합할 대상을 구분하기 위한 유니크한 ID 입니다.

javascript
1onSubmit() {
2 if (null === this.file) {
3 alert('파일을 선택하여 주세요.');
4 } else {
5 this.progress = 0;
6 this.result = null;
7
8 uploadService.chunk(
9 '/api/upload',
10 this.file,
11 // onProgress
12 percent => {
13 this.progress = percent;
14 },
15 // onError
16 err => {
17 alert('에러가 발생하였습니다!');
18 console.log(err);
19 },
20 // onSuccess
21 res => {
22 const { data } = res;
23 this.result = data.path;
24 }
25 );
26 }
27}

다시 onsubmit 이벤트 처리에서 아까 전에 정의한 메쏘드를 호출 합시다. (추가로 각 callback event 들을 연결시켜 줍니다.)

이렇게 파일 업로드 매니저가 뿅하고 탄생했습니다. bootstrap 를 이용해서 예쁘게 꾸며줍시다. 프로그레스바까지 꾸며주었습니다.

아! 너무 예쁘다...
아! 너무 예쁘다...

마치며

예제에 사용된 모든 소스 코드는 여기 에서 볼 수 있습니다. (axios는 너무나 좋은 툴입니다. 모두 쓰세오....)

출처 및 참고

Relate
Stories

mobx를 이용한 flutter 상태 관리

mobx를 이용한 flutter 상태 관리

coding
4 years ago, 8 Views

상태 관리란 무엇일까요? 위키 설명을 따르자면 텍스트 필드 같은 여러개의 UI 컨트롤의 상태를 관리하는 것을 의미합니다.

Read more

© 2022-2023 Yeppyshiba Blog. All rights reserved.

Akita inu icons created by tulpahn - Flaticon