| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- ModuleFederation
- styled-components
- 프론트엔드 개발
- useState
- 자바스크립트
- frontend
- turborepo
- pnpm
- capacitor
- 모노레포
- flutter
- typescript
- 타입스크립트
- Next.js
- 프론트엔드아키텍처
- useEffect
- 컴포넌트설계
- 상태관리
- mvvm
- 프론트엔드
- ionic
- CICD
- RECOIL
- react
- githubactions
- 웹개발
- a11y
- javascript
- styledcomponents
- vue3
- Today
- Total
yunkogong
[API 통신] XLSX 응답을 blob으로 처리하는 엑셀 다운로드 패턴 본문
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 } 단일 — 불필요한 추상화 방지
