React 파일 업로드 전략과 대용량 처리
React에서 대용량 파일 업로드와 multipart 처리, 프론트엔드 전략과 구현 예제를 중심으로 정리한 기술적 정리
목차
개요
파일 업로드는 사용자 경험과 인프라 비용에 직결되는 기능이다. 특히 대용량 파일 업로드는 네트워크 안정성, 브라우저 메모리, 서버 리소스, 보안 관점에서 고려사항이 많다. 이 글은 React 중심의 프론트엔드 업로드 전략과 multipart 처리, chunked/resumable 업로드 패턴을 실무 관점에서 설명한다.
핵심 원칙
효율적인 파일 업로드 설계는 다음 항목을 만족해야 한다.
- 브라우저 메모리와 네트워크 사용 최소화
- 업로드 중단 시 재개 가능성
- 서버 부하 분산과 보안 (예: presigned URL 사용)
- 사용자에게 실시간 진행 정보 제공
전략별 접근 방식
1) 단일 FormData 업로드
작은 파일(수십 MB 미만)에는 FormData 기반 업로드가 간단하다. 파일을 그대로 FormData에 담아 POST 요청으로 전송하면 된다. 장점은 구현이 단순하다는 점이고, 단점은 파일 크기가 커지면 실패 확률과 메모리 사용이 증가한다.
const uploadFile = async (file) => {
const form = new FormData();
form.append('file', file);
const res = await fetch('/api/upload', {
method: 'POST',
body: form
});
return res.json();
};
2) Presigned URL 방식
S3 같은 객체 스토리지로 직접 업로드할 때 presigned URL을 사용하면 서버 부하를 줄일 수 있다. 프론트엔드에서 인증된 presigned URL을 받아 파일을 PUT/POST로 전송한다. 이 방식은 서버가 파일 바이트를 처리하지 않으므로 비용과 지연이 감소한다.
3) Multipart / Chunked 업로드
대용량 파일은 작은 청크로 분할하여 업로드하는 방식이 유리하다. 각 청크를 병렬 또는 순차적으로 전송하고 서버(또는 스토리지)가 최종 조립을 수행한다. 재시도 로직과 체크섬 검증을 추가하면 신뢰성이 높아진다.
// 클라이언트 측 청크 업로드 기본 구조
const chunkUpload = async (file, chunkSize = 5 * 1024 * 1024) => {
const total = Math.ceil(file.size / chunkSize);
for (let index = 0; index < total; index++) {
const start = index * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const form = new FormData();
form.append('chunk', chunk);
form.append('index', index);
form.append('total', total);
await fetch(`/api/upload/chunk`, { method: 'POST', body: form });
}
};
실전 고려사항
재개(resume)와 체크포인트
업로드 중단 시 전체를 다시 올리는 방식은 비효율적이다. 각 청크 업로드 결과를 서버에 기록하거나 ETag와 같은 체크포인트를 사용하면 재개가 가능하다. 클라이언트는 업로드 상태를 주기적으로 조회해 남은 청크만 전송한다.
병렬 업로드와 동시성 제어
여러 청크를 병렬로 전송하면 전체 업로드 시간이 줄어든다. 다만 동시 요청 수가 많아지면 클라이언트나 서버에 부하가 걸린다. 보통 3~6개의 동시 전송이 균형이 좋다.
진행률 표시
사용자 경험 관점에서 진행률은 중요하다. Axios나 fetch의 ReadableStream을 활용해 전송 바이트를 계산하면 정확한 프로그레스 바를 표시할 수 있다. 청크 방식이면 각 청크 완료 시점에 누적 비율을 업데이트하면 된다.
보안과 인증
파일 업로드는 인증과 권한 관리가 필요하다. presigned URL을 사용할 경우 URL 수명과 업로드 크기 제한을 설정한다. 서버에서 파일 타입 검증, 바이러스 검사, 확장자 필터링을 적용하면 안전성이 높아진다.
간단한 React 예제
다음 예제는 FormData를 이용한 기본 업로드 컴포넌트와 진행률 표시의 골격을 보여준다.
function FileUploader() {
const [progress, setProgress] = React.useState(0);
const onChange = async (e) => {
const file = e.target.files[0];
const form = new FormData();
form.append('file', file);
// fetch로는 progress 이벤트가 없으므로 XHR 사용 예시
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload');
xhr.upload.onprogress = (ev) => {
if (ev.lengthComputable) {
setProgress(Math.round((ev.loaded / ev.total) * 100));
}
};
xhr.send(form);
};
return (
<div>
<input type="file" onChange={onChange} />
<div>업로드 진행: {progress}%</div>
</div>
);
}
서버 측 간단 흐름
서버는 세 가지 역할만 담당하면 충분하다.
- presigned URL 발급
- 청크 수신 및 상태 저장
- 최종 파일 조립 또는 스토리지에 등록
// Express 예시 라우트 골격
app.post('/api/upload/chunk', (req, res) => {
// 청크 저장, 메타데이터 기록, 필요 시 검증
res.json({ ok: true });
});
app.post('/api/upload/complete', (req, res) => {
// 모든 청크 합치기 또는 스토리지 조립 호출
res.json({ ok: true, url: '/path/to/file' });
});
결론
대용량 파일 업로드는 단순한 전송 이상의 설계가 필요하다. React에서는 FormData, presigned URL, 청크 업로드, 재개 로직을 상황에 맞게 조합하면 신뢰성과 효율성을 확보할 수 있다. 네트워크 환경과 비용 구조를 고려해 적절한 전략을 선택하면 운영 안정성과 사용자 경험을 동시에 개선할 수 있다.