yunkogong

[API 통신] XLSX 응답을 blob으로 처리하는 엑셀 다운로드 패턴 본문

프론트엔드 개발 가이드

[API 통신] XLSX 응답을 blob으로 처리하는 엑셀 다운로드 패턴

yun0415 2026. 6. 14. 11:02
반응형

1️⃣ 문제 상황

API 명세서에는 엑셀 다운로드 응답 타입이 다음과 같이 적혀 있었다.

// API 명세 (명세서 기준)
Response: {
  status: number;
  code: string;
  message: string;
}

이 명세를 그대로 믿고 axios.get()으로 호출하면 response.data는 텍스트나 깨진 문자열이 된다. 파일은 내려오지 않는다. 명세가 오래되었거나 업데이트가 누락된 케이스다.


2️⃣ 원인 확인 — DevTools 매직바이트

Network 탭 → 해당 요청 → Headers 탭에서 Content-Type을 확인한다.

Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet

Preview 탭에서 이진 데이터가 보이거나, Response 탭 첫 문자가 PK이면 XLSX 파일이다. XLSX는 ZIP 포맷 기반이라 50 4B 03 04 시그니처가 나타난다.

확인 방법 내용

Content-Type 헤더 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
Response 탭 첫 바이트 PK (50 4B 03 04) — XLSX 시그니처
Preview 탭 이진 데이터 표시, JSON으로 파싱 불가

⚠️ API 명세 ≠ 실제 응답인 경우는 드물지 않다. 새 기능을 연동할 때는 호출 전 Network 탭으로 응답 타입을 먼저 확인하는 습관이 필요하다.


3️⃣ blob 처리로 수정

responseType: 'blob'을 지정하면 axios가 응답을 Blob 객체로 받는다.

// lib/utils/apis/excel/excel-api.ts
export async function fetchExcelFile(
  params: ExcelExportParams
): Promise<{ blob: Blob }> {
  const response = await axiosInstance.get('/api/v1/excel/download', {
    responseType: 'blob',
    params,
  });
  return { blob: response.data };
}

📌 반환 타입을 { blob } 단일로 고정하면 호출부에서 추가적인 가공 없이 단순하게 쓸 수 있다. response.data 전체를 반환하는 것보다 의도가 명확하다.


4️⃣ triggerBlobDownload 유틸

다운로드 트리거는 별도 유틸로 분리해 재사용한다.

// lib/utils/trigger-blob-download.ts
export function triggerBlobDownload(blob: Blob, fileName: string): void {
  const url = URL.createObjectURL(blob);
  const anchor = document.createElement('a');
  anchor.href = url;
  anchor.download = fileName;
  document.body.appendChild(anchor);
  anchor.click();
  document.body.removeChild(anchor);
  URL.revokeObjectURL(url);
}

createObjectURL로 임시 URL을 생성하고, 프로그래밍 방식으로 클릭 후 revokeObjectURL로 즉시 해제한다. 해제 단계를 빠뜨리면 세션 중 임시 URL이 누적된다.

💡 일부 브라우저에서는 click() 직후 같은 동기 틱에 해제하면 다운로드가 시작되기 전에 URL이 사라져 간헐적으로 실패할 수 있다. 더 안전하게는 setTimeout(() => URL.revokeObjectURL(url), 0)로 한 틱 미루는 방법도 있다.


5️⃣ hook으로 조립

컨테이너에서 API를 직접 호출하지 않고, 도메인별 hook으로 감싼다.

// hooks/use-excel-download.ts
export function useExcelDownload(type: string) {
  const handleDownload = async (params: ExcelExportParams) => {
    const { blob } = await fetchExcelFile({ ...params, type });
    const fileName = `${type}_${dayjs().format('YYYYMMDD')}.xlsx`;
    triggerBlobDownload(blob, fileName);
  };

  return { handleDownload };
}

여러 도메인에 동일한 엑셀 기능이 필요한 상황을 가정하자. API 함수와 파라미터 빌더는 공용 lib/utils/apis/excel에 두고, 각 도메인의 hook이 type으로 분기해 조립하는 구조로 분리한다. 공통 로직이 여러 컨테이너에 복제되는 것을 막는다.


6️⃣ API 명세·응답 불일치 체크리스트

엑셀 외에도 PDF, ZIP 등 바이너리 파일 응답 전반에 이 패턴이 적용된다.

증상 원인 / 처리

response.data가 깨진 문자열로 보임 responseType: 'blob' 미설정
다운로드는 되지만 파일이 열리지 않음 Content-Disposition 헤더·파일명 인코딩 확인
API 명세에 JSON이라 적혀 있는데 파일 응답 DevTools Content-Type 먼저 확인
브라우저에서 파일이 열리지 않고 바로 닫힘 revokeObjectURL 타이밍 확인 (클릭 이전 해제 금지)

서버가 Content-Disposition: attachment; filename="..."로 파일명을 내려준다면, 클라이언트에서 직접 만들기보다 해당 헤더 값을 파싱해 쓰는 편이 서버와 파일명을 일치시키기 좋다.


7️⃣ 정리

엑셀 다운로드 구현 시 API 명세를 그대로 믿으면 예상치 못한 곳에서 막힌다. 실제 응답 타입은 DevTools Network 탭에서 Content-Type과 매직바이트로 직접 확인하는 것이 가장 빠르다.

패턴 요약:

  • responseType: 'blob' — axios가 응답을 Blob으로 수신
  • triggerBlobDownload 유틸 분리 — 재사용 + revokeObjectURL 메모리 해제 보장
  • API / 빌더는 공용 lib — 도메인 hook이 type으로 조립
  • 반환 타입은 { blob } 단일 — 불필요한 추상화 방지
반응형