downloadFile
메모리에 있는 데이터를 사용자 PC에 파일로 저장합니다.
import { downloadFile } from '@teamsparta/utils';
downloadFile({ file: '안녕하세요', name: '인사.txt', type: 'text/plain' });이 함수는 무엇인가
브라우저에는 "이 데이터를 이 이름의 파일로 저장해줘" 같은 직접적인 API가 없습니다. 그래서 보이지 않는 <a download> 링크를 만들어 자동 클릭하는 트릭을 씁니다. downloadFile은 그 트릭과 함께, 환경마다 다른 예외 케이스(macOS WebView, 메모리 정리, CSV 한글 깨짐 등)를 모두 처리해 한 줄로 호출할 수 있게 만든 유틸입니다.
시그니처
function downloadFile(options: DownloadFileOptions): void;
type DownloadFileOptions =
| {
file: string; // string은 MIME을 알 길이 없으므로
type: string; // type 필수
name?: string;
appendBOM?: boolean;
revokeTimeout?: number;
win?: Window;
}
| {
file: Blob | File; // Blob/File은 .type이 이미 있으므로
type?: string; // type 선택 (생략 시 blob.type 사용)
name?: string;
appendBOM?: boolean;
revokeTimeout?: number;
win?: Window;
};옵션
| 이름 | 타입 | 필수 | 의미 |
|---|---|---|---|
file | File | Blob | string | ✓ | 파일로 저장할 데이터. string은 type으로 Blob에 래핑됩니다. |
type | string | string 입력일 때만 | MIME 타입. Blob/File 입력 시 생략하면 입력 객체의 .type이 사용됩니다. |
name | string | 저장될 파일 이름. 미지정 시 File.name → "file-download" 순으로 폴백. | |
appendBOM | boolean | utf-8 텍스트 MIME일 때만 파일 앞에 BOM을 붙입니다. Excel에서 한글 CSV 깨짐 방지용. | |
revokeTimeout | number | 다운로드 후 ObjectURL을 해제하는 지연(ms). 기본 0 (다음 틱에 해제). | |
win | Window | 다운로드를 트리거할 window. iframe 내부에서 동작시킬 때 사용. 기본 window. |
입력 종류별 사용
string — 그 자리에서 만든 텍스트
type에 맞춰 자동으로 Blob에 래핑됩니다.
downloadFile({
file: 'name,age\n홍길동,30\n',
name: 'people.csv',
type: 'text/csv;charset=utf-8',
});Blob — 직접 만든 데이터
JSON, Excel, ZIP 등 코드에서 만든 데이터. Blob 자체에 .type이 들어 있으므로 type을 생략할 수 있습니다.
const data = JSON.stringify({ a: 1 }, null, 2);
const blob = new Blob([data], { type: 'application/json' });
downloadFile({ file: blob, name: 'data.json' });File — 사용자가 업로드한 파일을 다시 내려받기
<input type="file">로 받은 파일을 그대로 다운로드. type도 name도 생략 가능합니다.
downloadFile({ file: userFile });옵션 자세히
appendBOM — Excel 한글 CSV 깨짐 방지
한국 PC의 Excel은 표식이 없는 utf-8 CSV를 cp949로 잘못 해석해 한글을 깨뜨립니다. 파일 맨 앞에 BOM(3바이트 표식)을 붙이면 Excel이 utf-8로 정확히 인식합니다.
Before — 한글 깨짐
downloadFile({
file: '이름,점수\n홍길동,100\n',
name: '결과.csv',
type: 'text/csv;charset=utf-8',
});
// Excel에서 열면: ì´ë¦,ì 수 ...After — 정상 표시
downloadFile({
file: '이름,점수\n홍길동,100\n',
name: '결과.csv',
type: 'text/csv;charset=utf-8',
appendBOM: true,
});
// Excel에서 열면: 이름,점수 ...JSON/PDF 같은 비텍스트 MIME에서는 appendBOM: true를 줘도 무시되니 안전합니다(잘못 붙으면 JSON 파싱이 깨집니다).
revokeTimeout — 메모리 해제 타이밍
URL.createObjectURL로 만든 Blob URL은 명시적으로 해제하지 않으면 페이지가 닫힐 때까지 메모리에 남습니다. 함수가 알아서 해제하지만, 너무 빨리 해제하면 다운로드가 끊기므로 다음 틱(setTimeout(..., 0))에 해제합니다.
기본값(0)으로 충분한 경우가 대부분입니다. 일부 느린 환경에서 다운로드가 중간에 끊긴다면 더 큰 값을 줍니다.
downloadFile({
file: largeBlob,
name: 'big.zip',
type: 'application/zip',
revokeTimeout: 1000, // 1초 뒤 해제
});win — iframe 내부에서 다운로드
부모 페이지에서 iframe 내부의 다운로드를 트리거할 때만 사용. 일반 페이지에서는 신경 쓸 필요 없습니다.
const iframe = document.querySelector('iframe');
if (iframe?.contentWindow) {
downloadFile({
file: blob,
name: 'inside.pdf',
type: 'application/pdf',
win: iframe.contentWindow,
});
}자주 만나는 패턴
인증이 필요한 파일 다운로드
<a download> 만으로는 Authorization 헤더를 실을 수 없습니다. fetch로 받아 Blob으로 만든 뒤 넘깁니다.
const res = await fetch('/api/report.pdf', {
headers: { Authorization: `Bearer ${token}` },
});
const blob = await res.blob();
downloadFile({ file: blob, name: 'report.pdf' });외부 이미지(CORS) 다운로드
브라우저는 cross-origin 이미지에 대해 anchor의 download 속성을 무시합니다. 한 번 fetch해서 같은 origin의 Blob으로 변환해야 합니다.
const res = await fetch(imageUrl);
const blob = await res.blob();
downloadFile({ file: blob, name: 'photo.jpg' });Canvas 스냅샷 저장
canvas.toDataURL()이 만든 data: URL을 string으로 그대로 넘기면 base64 텍스트 자체가 파일이 되어버립니다. Blob으로 변환해서 넘깁니다.
const dataUrl = canvas.toDataURL('image/png');
const blob = await (await fetch(dataUrl)).blob();
downloadFile({ file: blob, name: 'snapshot.png' });클라이언트에서 만든 Excel
xlsx 등으로 만든 ArrayBuffer를 Blob에 담아 넘깁니다.
import * as XLSX from 'xlsx';
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, sheet, '결과');
const buffer = XLSX.write(workbook, { type: 'array' });
const blob = new Blob([buffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
});
downloadFile({ file: blob, name: '성적표.xlsx' });여러 파일을 ZIP으로 묶어 다운로드
jszip으로 묶고 결과 Blob을 넘깁니다.
import JSZip from 'jszip';
const zip = new JSZip();
files.forEach((file) => zip.file(file.name, file.data));
const blob = await zip.generateAsync({ type: 'blob' });
downloadFile({ file: blob, name: '묶음.zip' });동작 원리 (간단히)
- 입력 데이터를 Blob으로 통일하고 필요하면 BOM을 붙입니다.
- 저장 파일명을 결정합니다 (
name→File.name→"file-download"). - 보이지 않는
<a download>를 만들어 Blob URL을 연결하고 자동 클릭합니다. - macOS WebView 환경에서는
<a download>가 무시되므로window.open(폴백 시location.href)으로 Blob URL을 엽니다. - 다음 틱에
URL.revokeObjectURL로 메모리를 해제하고 임시<a>를 정리합니다.
제약과 책임 분담
이 함수는 메모리에 있는 데이터를 디스크에 저장하는 단계만 담당합니다. 다음은 호출자가 직접 처리합니다.
| 상황 | 호출자가 해야 할 일 |
|---|---|
| 원격 URL | fetch(url) → res.blob() → downloadFile |
| 인증 헤더 | fetch(url, { headers: ... }) → blob → downloadFile |
| Canvas Data URL | fetch(dataUrl) → blob → downloadFile (string으로 그대로 X) |
| ZIP 패키징 | JSZip.generateAsync({ type: 'blob' }) → downloadFile |
| 진행도 표시 | fetch 단계에서 ReadableStream/Response progress 측정 후 호출 |
또한 IE 및 구 Edge는 지원하지 않습니다(msSaveOrOpenBlob 분기 의도적 제거).
Returns
void — 다운로드 트리거 자체는 동기로 끝납니다. 실제 디스크 쓰기는 브라우저 다운로드 매니저가 비동기로 처리하며 JS는 그 결과를 알 수 없습니다(브라우저 보안 정책상 노출되지 않음).
