File API와 Blob — 파일 읽기, 다운로드, 미리보기
브라우저에서 파일을 다루는 일은 생각보다 자주 발생합니다. 이미지 업로드 미리보기, CSV 다운로드, 파일 드래그 앤 드롭 등이 모두 File API와 Blob을 기반으로 동작합니다.
Blob이란?
Blob(Binary Large Object)은 바이너리 데이터의 불변 객체입니다.
// 텍스트 Blob 생성
const textBlob = new Blob(["Hello, World!"], { type: "text/plain" });
console.log(textBlob.size); // 13
console.log(textBlob.type); // "text/plain"
// JSON Blob
const jsonBlob = new Blob([JSON.stringify({ name: "정훈" })], {
type: "application/json",
});
// 여러 조각 합치기
const combined = new Blob(["Part 1, ", "Part 2, ", "Part 3"]);
File 객체
File은 Blob을 확장한 객체로, 파일 이름과 수정 날짜 정보가 추가됩니다.
// input으로 파일 선택
const input = document.querySelector('input[type="file"]');
input.addEventListener("change", (e) => {
const file = e.target.files[0];
console.log(file.name); // "photo.jpg"
console.log(file.size); // 바이트 단위
console.log(file.type); // "image/jpeg"
console.log(file.lastModified); // 타임스탬프
});
// 프로그래밍적으로 File 생성
const file = new File(["내용"], "test.txt", {
type: "text/plain",
lastModified: Date.now(),
});
FileReader — 파일 읽기
function readFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsText(file); // 텍스트로 읽기
});
}
// 다양한 읽기 방식
const reader = new FileReader();
reader.readAsText(file); // 텍스트
reader.readAsDataURL(file); // Base64 Data URL
reader.readAsArrayBuffer(file); // ArrayBuffer
모던 방식 — Blob 메서드
// FileReader 없이 직접 읽기 (더 간결)
const text = await file.text();
const buffer = await file.arrayBuffer();
const stream = file.stream();
이미지 미리보기
// 방법 1: FileReader (Data URL)
function previewWithReader(file, imgElement) {
const reader = new FileReader();
reader.onload = (e) => {
imgElement.src = e.target.result;
};
reader.readAsDataURL(file);
}
// 방법 2: Object URL (더 효율적)
function previewWithObjectURL(file, imgElement) {
const url = URL.createObjectURL(file);
imgElement.src = url;
imgElement.onload = () => URL.revokeObjectURL(url); // 메모리 해제
}
Object URL은 Data URL보다 메모리 효율적이고 빠릅니다.
파일 다운로드
// 텍스트 파일 다운로드
function downloadText(content, filename) {
const blob = new Blob([content], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
downloadText("안녕하세요!", "greeting.txt");
// CSV 다운로드
function downloadCSV(data, filename) {
const csv = data.map((row) => row.join(",")).join("\n");
const bom = "\uFEFF"; // 한글 깨짐 방지 BOM
const blob = new Blob([bom + csv], { type: "text/csv;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
downloadCSV(
[["이름", "나이"], ["정훈", "25"], ["길동", "30"]],
"users.csv"
);
파일 크기 검증
function validateFile(file, options = {}) {
const { maxSize = 5 * 1024 * 1024, allowedTypes = [] } = options;
const errors = [];
if (file.size > maxSize) {
errors.push(`파일 크기가 ${formatBytes(maxSize)}를 초과합니다.`);
}
if (allowedTypes.length > 0 && !allowedTypes.includes(file.type)) {
errors.push(`허용되지 않는 파일 형식입니다: ${file.type}`);
}
return errors;
}
function formatBytes(bytes) {
if (bytes === 0) return "0 B";
const units = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
}
// 사용
const errors = validateFile(file, {
maxSize: 10 * 1024 * 1024, // 10MB
allowedTypes: ["image/jpeg", "image/png", "image/webp"],
});
Blob 슬라이싱 — 대용량 파일 분할 업로드
async function uploadInChunks(file, url, chunkSize = 1024 * 1024) {
const totalChunks = Math.ceil(file.size / chunkSize);
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("chunkIndex", i);
formData.append("totalChunks", totalChunks);
formData.append("fileName", file.name);
await fetch(url, { method: "POST", body: formData });
console.log(`업로드 진행: ${Math.round(((i + 1) / totalChunks) * 100)}%`);
}
}
fetch 응답을 파일로 저장
async function downloadFromServer(url, filename) {
const response = await fetch(url);
const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = objectUrl;
a.download = filename;
a.click();
URL.revokeObjectURL(objectUrl);
}
**기억하기 **: Blob은 바이너리 데이터 컨테이너이고, File은 Blob에 이름을 추가한 것입니다. 미리보기에는
URL.createObjectURL이 효율적이고, 다운로드에는 Blob +<a>태그 패턴을 사용합니다. Object URL은 사용 후 반드시revokeObjectURL로 해제해야 메모리 누수를 방지합니다.
댓글 로딩 중...