들어가며
파일 업로드 구현이야 많이들 해보셨겠지만 용량이 커진다면? 생각보다 신경쓸게 많아집니다.
- 낮은 서버 대기 시간
- 느린 클라이언트에게도 축복을
- 또한 멋진 업로드 매니저도 구현이 가능하구요
원리
원리는 정말 간단합니다. 파일을 그냥 짤라서 서버에 던지고, 서버에서는 파일을 합치면 됩니다.
물론 말은 쉽겠지만 코드로 이야기 하겠습니다.
백엔드
먼저 이 글의 중점은 프론트엔드이므로, 기존 라이브러리를 활용하도록 하겠습니다. 빠른 진행을 위해서 laravel 을 선택 했습니다.
shell1composer create-project --prefer-dist laravel/laravel
이 라이브러리가 쓸만한거 같더군요.
shell1composer require pion/laravel-chunk-upload
이제 업로드를 처리할 Controller 를 정의합니다. 업로드 진행시 퍼센트를 리턴해주며, 완료시에는 파일 병합 및 이동을 담당하게 됩니다.
php1class UploadController extends Controller2{3 //4 public function request(Request $request, FileReceiver $receiver)5 {6 if ($receiver->isUploaded() === false) {7 throw new UploadMissingFileException();8 }910 // receive the file11 $save = $receiver->receive();1213 // 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 need16 return $this->saveFile($save->getFile());17 }1819 $handler = $save->handler();2021 return response()->json([22 "done" => $handler->getPercentageDone(),23 "status" => true24 ]);25 }2627 /**28 * Saves the file29 *30 * @param UploadedFile $file31 *32 * @return \Illuminate\Http\JsonResponse33 */34 protected function saveFile(UploadedFile $file)35 {36 $fileName = $this->createFilename($file);3738 // Group files by the date39 $yearFolder = date('Y');40 $monthFolder = date('m');41 $filePath = "upload/{$yearFolder}/{$monthFolder}/";42 $finalPath = storage_path("app/public/{$filePath}");4344 // move the file name45 $file->move($finalPath, $fileName);4647 return [48 'path' => Storage::url($filePath . $fileName)49 ];50 }5152 /**53 * Create unique filename for uploaded file54 * @param UploadedFile $file55 * @return string56 */57 protected function createFilename(UploadedFile $file)58 {59 return implode([60 time(),61 mt_rand(100, 999),62 '.',63 $file->getClientOriginalExtension()64 ]);65 }66}
프론트엔드
별다른 설정 없이도 hot reload + webpack 을 지원하여 대!단!히! 편합니다.
input 태그를 담는 vue component 를 만들어 봅시다.
html1<input2 type="file"3 class="custom-file-input"4 accept="video/*,audio/*,image/*"5 ref="fileContainer"6 @change="onChangeFile"7/>
파일을 첨부하면, data 에 자동으로 등록되게끔 했습니다.
javascript1 onChangeFile() {2 const file = this.$refs.fileContainer.files;3 this.file = file.length > 0 ? file[0] : null;4 }
onsubmit 이벤트 시점에 axios 로 POST 호출하도록 설정하겠습니다.
javascript1const api = axios.create({2 headers: {3 'Content-type': 'application/x-www-form-urlencoded',4 Accept: 'application/json',5 },6});78const chunkSize = 1024 * 1024;
axios 개체를 만들어주고, 한번에 1Mb 씩 업로드하도록 사이즈를 지정했습니다.
javascript1const start = options.chunkNumber * chunkSize;2const end = Math.min(file.size, start + chunkSize);34let currentChunkSize = chunkSize;5if (options.chunkNumber + 1 === options.blockCount) {6 currentChunkSize = file.size - start;7}89const 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 객체를 만들어줍니다.
javascript1return api2 .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 을 실행하게 됩니다.
javascript1export 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('.', '')}`;67 return chunkUploader(endpoint, file, {8 blockCount,9 identifier,10 chunkNumber,11 onProgress,12 onError,13 onSuccess,14 });15 },16};
최초 업로드 요청을 처리하기 위한 함수를 정의합니다. identifier 는 병합할 대상을 구분하기 위한 유니크한 ID 입니다.
javascript1onSubmit() {2 if (null === this.file) {3 alert('파일을 선택하여 주세요.');4 } else {5 this.progress = 0;6 this.result = null;78 uploadService.chunk(9 '/api/upload',10 this.file,11 // onProgress12 percent => {13 this.progress = percent;14 },15 // onError16 err => {17 alert('에러가 발생하였습니다!');18 console.log(err);19 },20 // onSuccess21 res => {22 const { data } = res;23 this.result = data.path;24 }25 );26 }27}
다시 onsubmit 이벤트 처리에서 아까 전에 정의한 메쏘드를 호출 합시다. (추가로 각 callback event 들을 연결시켜 줍니다.)
이렇게 파일 업로드 매니저가 뿅하고 탄생했습니다. bootstrap 를 이용해서 예쁘게 꾸며줍시다. 프로그레스바까지 꾸며주었습니다.
마치며
출처 및 참고
- 참 쉽죠? - https://news.artnet.com/exhibitions/bob-ross-museum-debut-1537096
- UnderTale - https://undertale.com/
- resumablejs - http://www.resumablejs.com/
- laravel-chunk-upload - https://github.com/pionl/laravel-chunk-upload