대용량 파일 업로드 컴포넌트 — 설계 방향과 고려사항
구현 설명이 아니라 무엇을 어떤 순서로 결정해야 하는가를 정리한 문서입니다.
질문
여러 개의 대용량 파일을 업로드하는 UI는 어떻게 설계하며, 실무에서는 어떤 점을 고려해야 하나요?
답변 초안
어려움은 파일 크기가 아니라 시간에서 옵니다. 전송이 수십 초~수십 분 걸리는 동안 사용자는 파일을 더 추가하고, 취소하고, 탭을 옮기고, 새로고침합니다. 오래 걸리는 작업은 실패 확률도 누적됩니다. 그래서 설계의 무게중심은 정상 경로가 아니라 중단·재개·부분 실패에 있습니다.
흐름은 네 구간으로 끊어 봅니다. 눈에 보이는 3번에 신경이 쏠리지만, 실무에서 많이 새는 곳은 2번과 4번입니다.
[1] 받기 [2] 검증 [3] 전송 [4] 마무리
파일 선택 → 허용/거부 판단 → 진행률·재시도·취소 → 완료 / 실패 / 취소 확정
전송 구간에서 결정할 것이 아래 ①~⑤입니다. 한 문장으로: "오래 걸리는 작업을, 언제든 끊길 수 있다는 전제로 설계한다."
① 전송 단위: 언제 쪼개고, 언제 쪼개지 않는가
조각으로 나누면 실패의 단위가 파일 전체에서 조각 하나로 줄고 진행률 표현도 자연스러워집니다. 대신 서버 계약과 클라이언트 상태 관리가 늘어나므로 공짜가 아닙니다.
| 방식 | 적합한 상황 | 대가 |
|---|---|---|
| 단일 요청 | 수 MB 이하, 다시 올려도 무방 | 대용량에선 재전송 비용이 전부 |
| 조각 분할 | 수백 MB 이상, 이어올리기 필요 | 서버 계약 + 상태 관리 증가 |
| 스토리지 직접 업로드 | 서버 대역폭·타임아웃 회피 | 서명 URL·권한 설계, 규약 종속 |
판단 순서는 ① 최대 파일 크기 → ② 실패 시 처음부터 다시 올려도 되는가 → ③ 서버가 큰 요청 본문을 받는가(프록시 크기 제한·타임아웃이 실질 상한)입니다.
조각으로 간다면 정할 것 세 가지:
- 조각 크기 — 작으면 요청 수·오버헤드 증가, 크면 재시도 비용 증가. 설정값으로 빼둡니다.
- 조립 기준 — 조각에 순번을 붙이면 도착 순서가 섞여도 조립 가능. 없으면 순차 전송을 강제해야 하고 동시성 이득이 사라집니다.
- 재전송 안전성(멱등성) — 같은 조각을 두 번 보내도 결과가 같아야 합니다. 이 보장이 없으면 ③의 재시도·이어올리기 전략 전체가 무너지고, 할 수 있는 건 "처음부터 다시"뿐입니다.
서버와 먼저 합의할 것 — 중복 조각을 어떻게 처리하는가 / 중단된 업로드를 얼마나 보관하는가 (= 이어올리기 가능 시간) / 어디까지 올라갔는지 조회 가능한가(= 새로고침 후 재개 가능 여부) / 완료 응답이 곧 사용 가능인가(검사·변환이 뒤따르면 "처리 중" 상태가 하나 더 필요) / 실패 응답에서 재시도 가능한 실패와 영구 거부를 구분할 수 있는가.
② 동시성: 상한은 파일별이 아니라 전체 기준
파일마다 동시 전송을 허용하면 파일 개수만큼 연결이 늘어나 네트워크를 독점합니다. 전체를 아우르는 상한 하나를 모든 전송이 공유하면 파일 수와 무관하게 사용량이 예측 가능해집니다. 값은 브라우저의 호스트당 연결 제한과 서버가 감당할 동시 요청 수를 함께 보고 정하고, 설정값으로 빼둡니다.
파일 최대 개수 제한은 기술적 한계가 아니라 UX 가드레일입니다. 그리고 대기 중인 전송에도 상태가 필요합니다 — "시작 전"과 "전송 중"을 구분하지 않으면 대기열에 걸린 파일이 멈춘 것처럼 보입니다.
③ 실패: 자동으로 숨길 것과 사용자에게 넘길 것
1층 자동 재시도 — 일시적 네트워크 흔들림은 간격을 늘려가며(지수 백오프) 정해진 횟수만큼 조용히 다시 시도합니다. 간격 없이 연속 재시도하면 불안정한 상황을 악화시킵니다.
2층 사용자에게 넘기기 — 자동 재시도를 소진한 실패, 권한·용량 초과처럼 재시도로 해결되지 않는 실패는 드러냅니다. 필요한 정보는 무엇이 왜 실패했고 다음에 뭘 할 수 있는지입니다.
경계를 명시하지 않으면 양쪽으로 실패합니다. 너무 숨기면 멈춘 줄 알고 새로고침하고, 너무 드러내면 잠깐의 흔들림마다 에러가 튑니다. 그리고 재시도는 처음부터가 아니어야 합니다 — 성공한 조각을 기억해 남은 것만 이어 올립니다. 근거는 ①의 멱등성 보장입니다.
④ 중단: 취소·실패·이탈을 구분한다
- 사용자 취소 — 진행 중 요청을 정리하고 취소로 표시
- 복구 불가한 실패 — 정리는 같지만 실패로 표시. 취소와 뭉개면 "내가 취소하지 않았는데 취소됨"으로 보입니다
- 페이지 이탈·새로고침 — 진행 중이면 경고하고, 재개 가능 여부를 결정
공통 원칙은 중단 시 남은 요청을 반드시 정리하는 것입니다. 방치하면 대역폭이 새고 늦게 도착한 응답이 끝난 상태를 덮어씁니다.
경계 상황 둘 — 시작 전 취소: 대기열에만 있는 파일도 취소 가능해야 하므로 전송 시작 시점에 취소 여부를 확인하는 지점이 필요합니다. 완료와 취소가 겹칠 때: 서버가 이미 완료 처리했다면 성공으로 확정하는 편이 안전합니다. 서버와 클라이언트 상태가 어긋나는 것이 더 나쁩니다.
⑤ 상태 소유권: 업로드 상태는 화면보다 오래 산다
사용자가 탭을 옮기거나 다른 화면으로 이동해도 전송은 계속되어야 합니다. 상태를 화면 컴포넌트 안에 두면 언마운트·리렌더에 전송 로직이 얽힙니다. 방향은 전송 로직과 상태를 화면 밖의 독립 모듈에 두고 UI가 구독하는 것입니다.
UI 컴포넌트 보여주기만 (상태를 만들지 않는다)
↑ 구독
업로드 모듈 상태 · 전송 · 재시도 · 취소 (화면 기술과 무관)
얻는 것은 생명주기 분리(화면이 사라져도 전송 유지), 검증 가능성(취소·동시성 로직을 UI 없이 테스트), 이식성(별도 스레드·다른 화면 기술로 이동)입니다.
파일 하나의 상태 모델은 단순하게 유지합니다.
대기 → 전송 중 → 성공
├──→ 실패 ──(재시도)──→ 대기로 복귀
└──→ 취소
검증에서 거부된 파일은 이 모델에 넣지 않는 편이 깔끔합니다. 목록에 올릴 대상이 아니라 "왜 못 올리는지 알려줄 대상"이므로, 사유와 함께 따로 전달해 노출 방식은 호출부가 정하게 합니다.
실무 주의점
- 진행률은 정확도와 복잡도의 교환. 조각 단위 갱신은 단순하고 조각이 작으면 충분히 매끄럽습니다. 바이트 단위는 정확하지만 갱신이 잦고, 플랫폼에 따라 업로드 방향 진행 이벤트를 지원하지 않는 API도 있습니다. 부정확한 남은 시간 추정은 없는 것보다 나쁩니다.
- 부분 성공이 기본값. 파일별로 상태를 보여주고, "다음 단계 가능 조건"(전부 성공? 하나라도?)을 제품 요구사항으로 먼저 정합니다.
- 거부·실패 사유는 파일별로 구체적으로. "업로드 실패"가 아니라 "50MB를 초과했습니다".
- 드래그&드롭만으로 끝내지 않는다. 클릭 선택 경로가 항상 있고, 드롭 영역은 키보드로도 도달·실행 가능해야 하며, 진행 상황은 텍스트로도 읽혀야 합니다.
- 취소와 목록에서 제거를 구분한다. 한 버튼으로 합치면 무엇이 일어날지 예측할 수 없습니다.
- 모바일은 조건이 다르다. 백그라운드 전환 시 중단·지연되고, 셀룰러에서 바로 시작해도 되는지는 별도 판단입니다.
- 파일 수가 커지면 병목이 전송에서 목록 렌더로 옮겨간다. 수백 개 규모면 목록 가상화와 진행률 갱신 스로틀링이 필요합니다.
- 필요 없는 복잡도를 미리 들이는 것도 설계 실패다. 수 MB 파일 하나를 올리는 화면에 조각 분할과 재개 로직을 넣을 이유는 없습니다.
면접 답변
대용량 파일 업로드에서 어려운 부분은 파일이 크다는 것 자체가 아니라 전송이 오래 걸린다는 점이라고 생각합니다. 그 사이 사용자는 파일을 더 추가하거나 취소하고 페이지를 벗어나며, 네트워크가 흔들릴 확률도 누적됩니다. 그래서 정상 흐름보다 중단과 재개, 부분 실패를 먼저 설계합니다.
먼저 전송 단위를 정합니다. 파일이 작고 실패해도 다시 올려도 무방하면 요청 하나로 보내고, 수백 MB 이상이거나 처음부터 다시 올리게 하면 안 되는 경우에는 조각으로 나눕니다. 조각으로 나눌 때 가장 중요한 전제는 서버가 같은 조각을 여러 번 받아도 결과가 달라지지 않는 것입니다. 이 멱등성 보장이 없으면 재시도나 이어올리기를 설계할 수 없어서, 중단된 업로드의 보관 기간과 진행 상태 조회 가능 여부까지 백엔드와 먼저 합의해야 한다고 봤습니다.
동시 요청 상한은 파일별로 두지 않고 전체 기준으로 하나만 둡니다. 파일마다 동시성을 주면 파일 개수만큼 연결이 늘어나 네트워크를 독점하는데, 전체 상한을 공유하면 파일 수와 무관하게 사용량이 예측 가능해집니다. 실패는 두 층으로 나눠, 일시적 오류는 간격을 늘려가며 자동 재시도로 넘기고 그래도 실패하면 사유와 다음 행동을 사용자에게 보여줍니다. 이때 재시도는 성공한 조각을 건너뛰고 남은 부분만 이어 올립니다.
중단 처리에서는 사용자가 취소한 것과 오류로 중단된 것을 구분해서 표시해야 합니다. 둘을 같은 상태로 처리하면 사용자는 취소하지 않았는데 취소된 것처럼 받아들입니다. 어느 경우든 진행 중이던 요청은 반드시 정리해야 하고, 그렇지 않으면 대역폭이 낭비되고 늦게 도착한 응답이 이미 끝난 상태를 덮어씁니다. 마지막으로 업로드 상태와 전송 로직은 화면 밖의 독립 모듈에 두고 UI는 구독만 하게 했습니다. 업로드는 화면 하나보다 오래 살아 있고, 취소·재시도·동시성 로직은 UI와 얽히면 검증하기 어렵기 때문입니다.