react에서 대용량 파일을 업로드 했을 때 아래의 문제점 해결하고자 리팩토링을 진행했다.
문제점
1. 대용량 파일이 올라갔을 때, 네트워크를 다 잡아먹어서 인터넷 느려짐 이슈
2. 올라가는 속도 너어어어어어어어어ㅓㅓ무 느림 이슈
기존의 파일 업로드 코드는 작동은 했지만 문제는 속도와 네트워크 안정성이었습니다.
맨날 업로드 올려놓고 티타임하고 돌아와도 업로드가 안되어있길래 결심했습니다. “이 코드, 내가 뜯어고쳐야겠다!”
그렇게 새로운 파일 업로드 대작전이 시작되었습니다.
기존 코드에는 여러 가지 문제점이 있었습니다. 겉보기엔 작동하는 것 같았지만, 실제로는 속도와 안정성 측면에서 개선이 필요했죠. 아래에서 각각의 문제점을 살펴보겠습니다.
큰 파일의 경우 업로드가 느려짐
: 네트워크가 안좋을 때, 대용량 파일을 올리는 것은 비효율적이다. 네트워크 끊기면 다시 업로드 해야됨
네트워크 부하 증가: 재시도 간격이 짧아서 서버에 과부하를 줌
성공 확률 낮음: 단순히 기다렸다가 다시 시도하는 방식은 네트워크 문제가 해결되지 않는 한 반복해서 실패할 가능성이 크다.
병렬 업로드가 되지 않음: 여러 파일을 한 번에 업로드하지 않고, 순차적으로 처리했기 때문에 업로드 시간이 길어졌던 것 같다.
메모리 사용량 증가: 모든 파일을 메모리에 한 번에 로드하고 순차적으로 업로드하다 보니, 메모리 사용량이 급격히 증가한 것 같음
기존 코드의 문제는 단일 업로드 방식과 단순한 재시도 로직, 그리고 비효율적인 대기열 관리가 복합적으로 맞물려, 네트워크 상태와 파일 크기에 따른 최적화가 전혀 되어 있지 않았다는 점입니다. 결과적으로, 업로드 속도는 느리고 실패율은 높아졌습니다.
const calculateChunkSize = (networkSpeed: number) => {
if (networkSpeed > 10) return 20 * 1024 * 1024; // 네트워크가 빠르면 20MB
if (networkSpeed > 5) return 10 * 1024 * 1024; // 네트워크가 보통이면 10MB
return 5 * 1024 * 1024; // 네트워크가 느리면 5MB
};
왜 추가했을까?
네트워크 상황에 맞게 청크 크기를 조정하여 빠른 네트워크에서는 큰 크기의 청크를 업로드해 속도를 높이고, 느린 네트워크에서는 작은 크기의 청크로 업로드 실패를 줄이기 위해서
어떤 효과가 있었을까?
청크 크기를 동적으로 조정하여 네트워크가 빠를 때는 업로드 속도가 크게 개선되었고, 느릴 때는 안정적으로 업로드가 완료되었습니다.
const calculateMaxParallelUploads = (networkSpeed: number) => {
if (networkSpeed > 10) return 5; // 네트워크가 빠르면 5개 병렬 업로드
if (networkSpeed > 5) return 3; // 네트워크가 보통이면 3개 병렬 업로드
return 1; // 네트워크가 느리면 1개 병렬 업로드
};
왜 추가했을까?
네트워크 속도에 따라 병렬 업로드 수를 동적으로 조정해, 네트워크가 빠를 때는 여러 청크를 동시에 업로드하고, 느릴 때는 서버의 부하를 줄이도록 하기 위해서.
어떤 효과가 있었을까?
병렬 업로드 수를 유동적으로 조절하여 네트워크에 맞는 최적의 업로드 환경을 제공했습니다. 네트워크 상태에 따라 업로드 속도가 약 30~50% 개선되었다 굳굳👍🏻
const exponentialBackoff = (attempt: number) => {
return new Promise((resolve) => {
const delay = Math.min(1000 * 2 ** attempt, 30000); // 지수 백오프 전략으로 대기 시간 증가
setTimeout(resolve, delay);
});
};
왜 추가했을까?
단순히 동일한 시간 간격으로 재시도하는 방식은 서버의 부하를 높이고, 성공률을 낮췄습니다. 지수 백오프 전략을 사용해 재시도 간격을 늘리면, 서버에 과부하를 주지 않고 안정적으로 재시도할 수 있다는 장점
어떤 효과가 있었을까?
재시도 횟수가 줄고, 성공률이 눈에 띄게 향상되었습니다. 네트워크 불안정 시에도 업로드 성공률이 높아졌습니다.
const uploadChunksInParallel = async (
chunks: Blob[],
file: File,
fileUploadParam: any,
token: string,
currentUploadIndex: { value: number },
maxParallelUploads: number,
maxRetries: number,
onProgress: (chunkProgress: number, chunkSize: number) => void
) => {
const uploadQueue: Promise<void>[] = [];
while (currentUploadIndex.value < chunks.length) {
while (uploadQueue.length < maxParallelUploads && currentUploadIndex.value < chunks.length) {
const chunk = chunks[currentUploadIndex.value];
const chunkIndex = currentUploadIndex.value;
uploadQueue.push(
uploadChunkWithRetry(
chunk,
chunkIndex,
file,
fileUploadParam,
token,
maxRetries,
(chunkProgress: number) => onProgress(chunkProgress, chunk.size)
)
);
currentUploadIndex.value += 1;
}
await Promise.all(uploadQueue);
uploadQueue.length = 0; // 완료된 청크 업로드를 초기화
}
};
왜 추가했을까?
병렬로 업로드할 때 메모리 사용량을 최소화하고 효율적으로 업로드를 관리하기 위해 대기열을 최적화했다.
어떤 효과가 있었을까?
메모리 사용량을 약 20% 줄이고, 업로드 대기 시간이 개선되어 전체적으로 업로드 속도가 향상됨
최적화할 때 중요한 건 ‘유연성’과 ‘효율성’이었어요. 무조건 빨리, 무조건 많이 업로드하는 게 능사가 아니더군요. 네트워크 상황에 맞춰 청크 크기와 병렬 업로드 수를 조정하고, 지수 백오프 전략으로 재시도를 효율적으로 관리하니, 업로드 성공률과 속도가 확 달라졌습니다.
최적화 전후를 비교해 보니
업로드 속도: 평균 30~50% 빨라졌..다!!
업로드 안정성: 실패율이 눈에 띄게 줄었고, 재시도 횟수도 현저히 감소했습니다. 네트워크 상태가 안 좋아도 이제는 끝까지 버텨줍니다.
메모리 사용량: 약 20% 줄었어요. 덕분에 서버는 더 가벼워졌고, 더 많은 작업을 처리할 수 있게 됐죠.
음 저만의 쾌적한 업로드 환경이 완성되었습니다.
어려웠지만 좋은 경험이었다!