Skip to Content
Backend Architecture진행 상태 피드백 패턴

다운로드 진행 상태 피드백 패턴

개요

파일/엑셀 다운로드처럼 서버 처리 시간이 긴 작업에서 사용자에게 “진행 중”임을 알리는 4가지 패턴(인디케이터, 단계별 진행률, Polling, SSE)을 정리한다. 각 방식의 장단점과 작업 시간 기준 업계 관행을 비교하여, 어떤 상황에 어떤 방식을 채택할지 의사결정 기준을 제공한다.

배경: 다운로드 버튼 클릭 후 아무 피드백이 없으면 사용자가 (1) 오류로 인식하거나 (2) 중복 클릭하는 문제가 발생한다. 이를 해결하는 것이 진행 상태 피드백의 목적이다.

핵심 개념: “진짜 진행률”이 가능한가?

진행률(%)을 표시하려면 서버가 중간 진행 상태를 전달할 수 있는 구조여야 한다.

[진짜 % 불가능한 구조 — 대부분의 단순 API] 요청 → (서버가 통째로 처리) → JSON 한 번에 응답 → 클라이언트에서 파일 생성 └ 이 구간 동안 진행 정보를 받을 방법이 없음 (0%였다가 갑자기 100%) [진짜 % 가능한 구조] 요청 → 서버가 작업을 쪼개서 진행 상태를 별도 채널(Redis/스트림)로 노출 → 클라이언트가 그 상태를 주기적/실시간으로 수신

즉, 진짜 진행률은 프론트엔드만으로는 불가능하고 백엔드가 진행 상태를 외부에 노출해야 한다.


4가지 패턴 비교

방식 1: 인디케이터만 (Indeterminate)

버튼 비활성화 + 스피너 + 완료 Toast만 표시. 진행률 숫자는 없음.

  • 장점: 구현 가장 단순, BE 변경 0, 기존 코드 영향 없음, 중복 클릭 방지
  • 단점: 진행 상태 정보 없음, 대용량 시 “멈춘 건지” 불안감
  • 업계 사례: GitHub 파일 업로드, Google Drive 소용량 다운로드
  • 적합 조건: 대부분 작업이 3초 이내로 끝날 때
// indeterminate 로딩 바는 CSS 애니메이션만으로 구현 → BE 무관 <Button label={isLoading ? '다운로드 중...' : '엑셀 다운로드'} disabled={isLoading} loading={isLoading} onClick={handleDownload} />

방식 2: 단계별 진행률 (Staged / Fake Progress)

코드 실행 단계에 진행률을 수동 매핑. 0 → 30 → 70 → 100%.

  • 장점: BE 변경 없음, 100% = 진짜 완료 보장, 구현 단순
  • 단점: “가짜 진행률” (실제 처리량 반영 X), 단계 사이에서 멈춰 보일 수 있음
  • 업계 사례: Figma 내보내기, Notion PDF 내보내기 — 실제로 매우 널리 쓰임
  • 적합 조건: 작업 시간이 예측 가능하고 단계가 명확할 때
버튼 클릭 → 0% "데이터 요청 중..." API 응답 → 60% "파일 생성 중..." ← 실제로 데이터 도착한 시점 XLSX 생성 → 90% "저장 준비 중..." writeFile → 100% "완료!" ← 실제로 파일 저장된 시점

각 단계가 실제 코드 실행에 1:1 대응하므로, 사용자가 100%를 보는 순간 파일은 이미 저장돼 있다.

방식 3: Polling (주기적 상태 조회)

BE가 비동기 Job을 만들고 진행 상태를 Redis 등에 저장. FE가 일정 간격(예: 0.5초)으로 상태를 조회.

① FE 다운로드 요청 → BE가 즉시 { jobId } 반환 ② BE 백그라운드 처리 → 진행률을 Redis에 저장 (TTL 설정) ③ FE 주기적 polling → GET /jobs/{jobId} → { progress: 40, status: PROCESSING } ④ status: DONE → 결과 데이터/파일 URL로 다운로드
  • 장점: 진짜 진행률, 서버 상태 정확 반영, 연결 끊겨도 재연결 쉬움(stateless), 기존 인증 구조 그대로 사용
  • 단점: BE 신규 개발(Job 관리 + Redis), FE 폴링 로직, 네트워크 요청 증가
  • 업계 사례: AWS S3 대용량 업로드, Stripe 정산 배치, Shopify 대량 내보내기 — 대용량 비동기 작업의 사실상 업계 표준
  • 적합 조건: 작업 시간 10초 이상이거나 실패 재시도/백그라운드 처리가 중요할 때

방식 4: SSE (Server-Sent Events)

BE가 스트리밍 응답으로 진행 상태를 push, FE는 EventSource로 수신.

  • 장점: 가장 실시간, 서버 push 방식, 연결 1개로 효율적
  • 단점: 아래 “SSE의 함정” 참고. HTTP/1.1에서 브라우저 동시 연결 수 제한(6개)
  • 업계 사례: ChatGPT 스트리밍 응답, GitHub Actions 로그 실시간 표시 — 실시간성이 핵심일 때
  • 적합 조건: 1초 이하 실시간성이 필요하거나, 서버가 작업 중 중간 결과를 계속 생성할 때

SSE의 함정: 인증 토큰

SSE는 Spring Boot의 SseEmitter로 서버 구현은 쉽지만, 브라우저 측 인증 처리가 까다롭다.

// 표준 EventSource는 커스텀 헤더 설정이 불가능 const es = new EventSource('/api/stream'); // ❌ Authorization: Bearer 헤더를 붙일 방법이 없음 (GET only, 헤더 설정 불가)

Keycloak/OAuth2처럼 모든 요청에 Bearer 토큰이 필요한 시스템에서는 우회가 필요하다:

우회 방법문제점
토큰을 쿼리스트링으로 ?token=xxxURL/로그에 토큰 노출 → 보안 위반
fetch + ReadableStream 직접 구현EventSource 대체 코드를 직접 작성
프록시(BFF) 서버에서 SSE 통과 처리프록시를 스트리밍 모드로 대폭 수정

핵심: SSE가 “불가능”한 게 아니라, 인증 게이트웨이/프록시 구조를 쓰는 프로젝트에서는 FE 구현 비용이 크다. Polling은 기존 HTTP API 클라이언트를 그대로 쓰므로 토큰 처리가 자동으로 해결된다. 이것이 비동기 다운로드에서 Polling이 더 자주 선택되는 실질적 이유다.


의사결정 기준 (작업 시간 기준)

~3초 → 방식 1 (인디케이터) 3~10초 → 방식 2 (단계별) 또는 방식 3 (Polling) 10초+ → 방식 3 (Polling) 강력 권장 실시간성이 핵심 / 중간 결과 생성 → 방식 4 (SSE)

종합 비교표:

항목인디케이터단계별PollingSSE
진짜 진행률△ (가짜)
BE 변경없음없음큼(Job+Redis)중간(스트림)
FE 변경작음작음중간(폴링)큼(인증/프록시)
인증 처리자동자동자동까다로움
실시간성--0.5초 수준최고
유지보수쉬움쉬움보통어려움

실무 트레이드오프 결론

  • 단순/소용량: 방식 1, 2가 비용 대비 효과 최고. Notion·Figma 같은 B2B SaaS도 실제로 단계별 방식을 많이 쓴다.
  • 대용량/비동기/재시도 필요: 방식 3 Polling. Redis가 이미 인프라에 있으면 자연스러운 선택.
  • SSE는 채팅·로그 스트리밍처럼 실시간성이 본질인 경우에만. 인증 프록시 환경에서 단순 다운로드 진행률 용도로는 과한 경우가 많다.
  • 처음부터 과설계하지 말고, 작업 시간이 실제로 길어질 때 방식 2 → 방식 3으로 업그레이드하는 점진적 접근이 안전하다.

참고

Last updated on