대용량 파일을 웹 애플리케이션에서 업로드하는 기능을 구현하려면 몇 가지 중요한 도전 과제가 있습니다. 특히 파일의 크기가 수십 기가바이트에서 수백 기가바이트에 이를 때, 효율적이고 안정적인 업로드 방법을 찾아야 합니다. 이 글에서는 대용량 파일 업로드의 기본 개념과 함께 웹 워커를 활용하여 프론트엔드에서 백그라운드로 파일을 업로드하는 방법을 알아보겠습니다.
1. 대용량 파일 업로드의 도전 과제
대용량 파일을 웹 애플리케이션에서 업로드할 때 직면하는 주요 도전 과제는 다음과 같습니다:
- 네트워크 안정성: 큰 파일은 업로드하는 데 시간이 많이 걸리며, 도중에 네트워크 연결이 불안정해지거나 끊길 가능성이 있습니다.
- 서버 리소스 관리: 서버는 동시에 여러 사용자가 대용량 파일을 업로드할 수 있도록 준비되어야 하며, 충분한 스토리지와 메모리 관리가 필요합니다.
- UI 반응성: 대용량 파일 업로드 작업은 클라이언트 측에서 상당한 자원을 소모하므로, UI가 느려지거나 응답하지 않게 될 수 있습니다.
이러한 문제를 해결하기 위해, 파일을 청크로 분할하여 업로드하는 기술과 웹 워커(Web Workers)를 활용한 백그라운드 처리를 결합한 방법을 사용합니다.
2. 파일을 청크로 분할하여 업로드
대용량 파일을 업로드할 때 하나의 큰 파일을 한 번에 전송하는 대신, 파일을 작은 청크(조각)로 나누어 순차적으로 업로드하는 방법을 사용할 수 있습니다. 이렇게 하면 다음과 같은 이점이 있습니다:
- 네트워크 오류 복구 용이성: 청크 단위로 업로드를 재시도할 수 있어, 전체 업로드 과정이 실패하지 않고 부분적으로 진행 가능합니다.
- 병렬 업로드 가능: 여러 청크를 동시에 업로드하여 전체 업로드 시간을 단축할 수 있습니다.
3. 웹 워커를 이용한 백그라운드 업로드 처리
웹 워커(Web Workers)는 JavaScript 코드를 메인 스레드와 분리하여 백그라운드에서 실행할 수 있게 해주는 기능입니다. 이를 통해 파일 업로드 작업이 UI의 반응성을 저해하지 않도록 만들 수 있습니다.
웹 워커를 사용한 대용량 파일 업로드 구현의 주요 단계는 다음과 같습니다:
- 파일 선택 및 준비: 사용자가 파일을 선택하면, 클라이언트 측에서 파일을 청크로 분할합니다.
- 백그라운드 작업 시작: 웹 워커를 생성하여 파일 업로드 작업을 백그라운드에서 처리합니다.
- 청크 업로드: 웹 워커는 파일 청크를 순차적으로 또는 병렬로 서버에 업로드하며, 업로드가 성공적으로 완료될 때마다 메인 스레드로 상태 메시지를 전송합니다.
- 서버에서 청크 병합: 서버는 수신된 청크를 순서대로 병합하여 최종 파일을 완성합니다.
4. 구현 예제
서버 측 코드 (Flask)
서버에서는 클라이언트로부터 청크를 수신하고 이를 병합하여 최종 파일로 저장하는 역할을 합니다.
from flask import Flask, request, jsonify
import os
app = Flask(__name__)
UPLOAD_FOLDER = 'uploads'
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
@app.route('/upload', methods=['POST'])
def upload_chunk():
file = request.files['file']
chunk_index = request.form['chunkIndex']
total_chunks = request.form['totalChunks']
file_name = request.form['fileName']
chunk_path = os.path.join(UPLOAD_FOLDER, f'{file_name}_chunk_{chunk_index}')
file.save(chunk_path)
if int(chunk_index) + 1 == int(total_chunks):
merge_chunks(file_name, total_chunks)
return jsonify({"message": "Chunk uploaded successfully"})
def merge_chunks(file_name, total_chunks):
with open(os.path.join(UPLOAD_FOLDER, file_name), 'wb') as final_file:
for i in range(int(total_chunks)):
chunk_path = os.path.join(UPLOAD_FOLDER, f'{file_name}_chunk_{i}')
with open(chunk_path, 'rb') as chunk_file:
final_file.write(chunk_file.read())
os.remove(chunk_path)
if __name__ == '__main__':
app.run(debug=True)
클라이언트 측 코드 (HTML & JavaScript)
HTML과 JavaScript로 작성된 클라이언트 측 코드는 파일을 선택하고 웹 워커를 사용해 파일 업로드 작업을 백그라운드에서 수행합니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Background File Upload</title>
</head>
<body>
<input type="file" id="fileInput">
<button onclick="startUpload()">Start Upload</button>
<p id="status">Waiting to start...</p>
<script src="main.js"></script>
</body>
</html>
let worker;
function startUpload() {
const file = document.getElementById('fileInput').files[0];
if (file) {
worker = new Worker('worker.js');
worker.postMessage({ file });
worker.onmessage = function(event) {
document.getElementById('status').textContent = event.data.message;
};
worker.onerror = function(error) {
console.error('Worker error:', error);
document.getElementById('status').textContent = 'Error occurred during upload.';
};
} else {
alert('Please select a file first.');
}
}
const CHUNK_SIZE = 10 * 1024 * 1024; // 10MB
self.onmessage = async function(event) {
const file = event.data.file;
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
let chunkIndex = 0;
while (chunkIndex < totalChunks) {
const start = chunkIndex * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('file', chunk);
formData.append('chunkIndex', chunkIndex);
formData.append('totalChunks', totalChunks);
formData.append('fileName', file.name);
try {
await fetch('/upload', {
method: 'POST',
body: formData,
});
self.postMessage({ message: `Uploaded chunk ${chunkIndex + 1} of ${totalChunks}` });
chunkIndex++;
} catch (error) {
self.postMessage({ message: `Error uploading chunk ${chunkIndex + 1}` });
break;
}
}
if (chunkIndex === totalChunks) {
self.postMessage({ message: 'File upload complete' });
}
};
5. 실행 및 결과
- Flask 서버를 실행하여 클라이언트의 업로드 요청을 받을 준비를 합니다.
- 브라우저에서 클라이언트 코드를 실행하고 파일을 선택한 뒤 `Start Upload` 버튼을 누르면, 웹 워커가 파일 업로드를 백그라운드에서 처리합니다.
- 업로드 상태가 실시간으로 UI에 표시되며, 업로드 중에도 사용자는 다른 작업을 계속 수행할 수 있습니다.
결론
대용량 파일 업로드는 웹 애플리케이션에서 중요한 기능 중 하나입니다. 웹 워커를 활용하여 파일 업로드를 백그라운드에서 처리함으로써, 사용자는 업로드 도중에도 UI의 다른 기능을 계속 사용할 수 있습니다. 이 방법은 UI 반응성을 유지하면서도 안정적인 업로드 경험을 제공하는 데 효과적입니다.