Canvas 이미지 다운로드하기 - Base64 방식
export const downloadCanvasImage = (canvas: HTMLCanvasElement, filename = "edited-image.png") => {
const link = document.createElement("a");
link.href = canvas.toDataURL("image/png");
link.download = filename;
link.click();
};
- Canvs에 그려진 이미지를 PNG 파일로 변환하고
- 브라우저에서 바로 다운로드하도록 함
<a> 태그(링크) 만들기
const link = document.createElement("a");
- 다운로드 기능을 만들려면 링크 클릭 이벤트 사용
- 동적으로 a 태그를 만들어서 브라우저가 다운로드 이벤트 처리
<a> 태그가 필요한 이유
- 브라우저는 JS 코드가 임의 파일을 마음대로 다운로드하도록 허용하지 않음
- 사용자가 링크를 클릭했다는 이벤트를 통해 브라우저가 다운로드를 허용해줌
- <a href="..."> 링크가 파일을 가리키고
- <a download="파일명"> 속성이 붙으면
- 사용자가 링크를 클릭한 것으로 처리해서 다운로드를 시작함
브라우저가 다운로드를 처리하는 내부 로직
1. <a> 태그의 href를 확인
- href가 data:image/png;base64,... 같은 blob/data URL이면 이걸 하나의 파일로 간주
2. download 속성이 있는지 체크
<a href="..." download="edited.png">
- 브라우저는 다운로드 링크로 인식
- download="" 없이 클릭하면 그냥 화면에 표시하거나 페이지 이동
- download="파일명"이 있으면 다운로드용 네비게이션 요청으로 처리
3. 링크 클릭 이벤트가 발생해야 다운로드가 허용됨
link.click();
- 보안 때문에 무조건 사용자 제스처가 있어햐 다운로드 허용
- JS에서 강제로 클릭해줌
4. 브라우저 자체가 다운로드 프로세스를 수행
- download 네비게이션이 발생하면 브라우저는
- href 데이터를 파일로 인식
- 다운로드 매니저로 넘김
- 파일 저장 창을 띄우거나 즉시 다운로드
Canvas 내용을 Data URL(이미지 Base64)로 변환
link.href = canvas.toDataURL("image/png");
- canvas.toDataURL()은 캔버스의 현재 이미지를 Base64 문자열로 변환
- image/png는 PNG 형식으로 변환
Base64란?
- 바이너리 데이터를 텍스트로 바꾸는 인코딩 방식
- 원래 이미지는 0과 1로 이루어진 바이너리 데이터
- Base64는 이걸 문자로 표현함
data:image/png;base64,ividsaivi...
- 사람이 읽을 수 있는 문자열
- ASCII 표준 문자만 사용
- 바이너리 파일을 텍스트 환경에서도 안전하게 전달 가능
Base64가 필요한 이유
- 브라우저는 다운로드할 파일을 반드시 URL 형태로 요구하기 때문
방법 1: 서버에 이미지를 업로드해서
- href="http://example.com/image.png"
- 서버 필요해서 즉시 다운로드하기 어렵고 서버 I/O 비용이 듦
방법 2: 클라이언트에서 파일을 문자열 URL로 인코딩
- 이게 Base64
- Canvas의 이미지 필섹 데이터를 브라우저가 이해하는 텍스트 URL로 바꿔야 함
Canvas는 Base64로 이미지 변환하는 기능을 가지는 이유
- Canvas는 웹 브라우저 안에서 직접 그린 이미지라서 기본적으로 픽셀 배열(buffer) 형태로 존재
- [R, G, B, A, R, G, B, A, ...] 이런 식의 unit8 배열
- 이런 바이너리 배열을 브라우저는 직접 다운로드 파일로 사용할 수 없음
canvas.toDataURL("image/png");
- Canvas 픽셀 데이터를 읽어서
- PNG 파일 형태로 인코딩하고
- Base64 문자열 형태로 변환한 뒤
- URL처럼 보이는 문자열 생성해 줌
Base64를 사용하는 이유
- 서버 없이도 즉시 파일을 URL로 만들 수 있음
- <a href="...">에서 바로 다운로드 가능
- 이미지, 오디오, JSON 어떤 바이너리든 문자열로 담을 수 있음
- 클라이언트 안에서 이미지 저장을 가능하게 해주는 유일한 방식 중 하나
Base64 단점
- 파일 크기 약 33% 증가함
- 원문: 3바이트
- Base64: 4문자(=4바이트)
- 큰 이미지를 Base64로 쓰면 메모리/성능 부하
- 대규모 이미지 다운로드는 Blob 방식이 더 효율적
파일 이름 설정
link.download = filename;
- 브라우저에게 이 링크가 파일 다운로드용이라고 알려줌
- 사용자가 저장할 때 표시될 파일명 지정
- 기본값은 edited-image.png
강제로 링크 클릭해서 다운로드 실행
link.click();
- 사용자 클릭 없이도 프로그램적으로 클릭 이벤트를 발생시켜서 다운로드 시작
- 실제로 화면에 <a> 태그를 보여줄 필요 없음
Canvas 이미지 다운로드하기 - Blob(Binary Large Object) 방식
- 파일 크기 증가 없음
- 훨씬 빠름
- 메모리 사용량도 적음
- 웹에서 실제 파일 객제처럼 다룸
Blob 방식이란?
- 브라우저가 기본적으로 제공하는 파일과 같은 바이너리 데이터 객체
- Base64는 텍스트 문자열이지만
- Blob는 진짜 바이너리 데이터 형태로 저장됨
- 브라우저는 Blob을 파일처럼 취급해서 다운로드 가능
export const downloadCanvasImageBlob = (canvas: HTMLCanvasElement, filename = "edited-image.png") => {
canvas.toBlob((blob) => {
if (!blob) return;
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url); // 메모리 해제
}, "image/png");
};
Canvas를 Blob으로 변환
canvas.toBlob(...)
- Canvas 내용을 PNG 바이너리 파일(Blob)로 변환함
Blob을 브라우저가 이해하는 URL로 만들기
URL.createObjectURL(blob)
- Blob -> 임시 가짜 URL(blob://...) 생성
- 이 URL은 실제 서버 파일처럼 동작함
- blob:http://localhost:3000/asdfsadfsda31212
<a> 태그로 다운로드 트리거
link.href = url;
link.download = filenname;
link.click();
- 즉시 다운로드 시작
메모리 해제
URL.revokeObjectURL(url);
- Blob URL은 메모리를 차지하므로
- 다운로드 후 반드시 제거해야 함
| 항목 | Base64 | Blob |
| 파일 크기 | 33% 증가 | 그대로 |
| 속도 | 느림 | 빠름 |
| 메모리 | 많이 먹음 | 적게 먹음 |
| 대규모 이미지 | 비효율적 | 추천 |
| 동작 방식 | 텍스트 URL | 바이너리 URL |
| 표준성 | 널리 사용됨 | 최신 기술이지만 권장 |
iOS Safari 대응 버전
문제 1: toBlob() 지원하지 않을 때 있음
- Polyfill 필요 (canvas.toDataURL() -> Blob 변환)
문제 2: <a download> 속성 미지원
- 새 창에서 이미지 열어 사용자에게 길게 누르기 -> 이미지 저장 방식 제공
export const downloadImageiOSSafariSafe = async (
canvas: HTMLCanvasElement,
filename = "image.png"
) => {
// 1) iOS Safari인지 체크
const isIOS =
/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
// 2) toBlob 지원 안 하는 브라우저를 위해 폴리필 적용
const blob =
await new Promise<Blob | null>((resolve) => {
if (canvas.toBlob) {
canvas.toBlob((b) => resolve(b), "image/png");
} else {
// 폴리필: Base64 → Blob
const dataURL = canvas.toDataURL("image/png");
resolve(dataURLToBlob(dataURL));
}
});
if (!blob) return;
// 3) iOS Safari는 <a download> 미지원
if (isIOS) {
const url = URL.createObjectURL(blob);
// 새 창에서 열기 (사용자 저장)
const win = window.open(url, "_blank");
if (!win) alert("팝업 차단 해제 후 다시 시도하세요.");
return;
}
// 4) 일반 브라우저 (Chrome, Edge, Firefox, Android)
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
};
// ▼ Base64 → Blob 변환 유틸
const dataURLToBlob = (dataURL: string): Blob => {
const parts = dataURL.split(",");
const mime = parts[0].match(/:(.*?);/)![1];
const binary = atob(parts[1]);
const len = binary.length;
const arr = new Uint8Array(len);
for (let i = 0; i < len; i++) {
arr[i] = binary.charCodeAt(i);
}
return new Blob([arr], { type: mime });
};
- 어떤 브라우저든 Canvas 이미지를 다운로드할 수 있게 만듦
- Chrome / Edge / Firefox / Android
- iOS Safari
- toBlob() 지원 안 되는 브라우저
- <a download> 지원 안 되는 브라우저
1. iOS Safari인지 체크
const isIOS =
/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
- iOS Safari는 다음 문제를 별도로 처리해야 함
- <a download> 속성 미지원
- Blob URL 다운로드 동작 제한
- 파일 다운로드를 JS에서 직접 트리거 불가능
- 새 창에서 이미지 띄우기 -> 길게 누르기 ->저장 방식만 가능
- 유저 에이전트로 iOS 기기를 판별
2. canvas.toBlob() 지원 여부 확인 + 폴리필
const blob =
await new Promise<Blob | null>((resolve) => {
if (canvas.toBlob) {
canvas.toBlob((b) => resolve(b), "image/png");
} else {
// 폴리필: Base64 → Blob
const dataURL = canvas.toDataURL("image/png");
resolve(dataURLToBlob(dataURL));
}
});
이유
- 일부 브라우저는 canvas.toBlob() 지원 안 함
- canvas.toBlob() 방식 지원 또는
- canvas.toDataURL() -> Blob 변환
동작
- 가능하면 toBlob 사용 -> Blob 직접 생성 (권장)
- 불가능하면 Base64 -> Blob 변환 (폴리필)
- Promise로 묶어서 await 사용 가능하게 만듦
3. iOS Safari 전용 처리
if (isIOS) {
const url = URL.createObjectURL(blob);
const win = window.open(url, "_blank");
if (!win) alert("팝업 차단 해제 후 다시 시도하세요.");
return;
}
왜 iOS에서는 다운로드가 안될까?
- <a download> 불가능
- Blob URL 직접 다운로드 불가능
- JS로 다운로드 트리거 불가능
- 자동 다운로드 전부 차단
해결책
- 새 창을 열어서 사용자에게 이미지를 보여주고
- 유저가 사진 저장을 직적 해야 함
코드 동작
- Blob -> URL 변환
- 새 창에서 이미지 보여주기
- iOS 유저가 직접 저장
- 팝업 차단된 경우 경고 메시지 출력
4. 일반 브라우저 다운로드 처리
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
- Blob을 가짜 URL로 변환
- <a download> 속성을 이용해 파일 다운로드 트리거
- link.click()으로 프로그램적으로 링크 클릭 이벤트 실행
- 다운로드가 끝나면 URL 메모리 해제
5. Base64 -> Blob 변환 유틸
const dataURLToBlob = (dataURL: string): Blob => {
const parts = dataURL.split(",");
const mime = parts[0].match(/:(.*?);/)![1];
const binary = atob(parts[1]);
const len = binary.length;
const arr = new Unit8Array(len);
for (let i = 0; i < len; i++) {
arr[i] = binary.charCodeAt(i);
}
return new Blob([arr], { type: mime });
};
Base64 DataURL 구조 이해하기
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...
- data: -> DataURL 시작을 의미
- image/png -> MIME 타입 (png)
- base64 -> Base64로 인코딩 되어 있음
- 나머지 -> 실제 이미지 바이트를 Base64 문자열로 표현한 것
- 이 문자열을 다시 0과 1의 바이너리 데이터로 되돌리는 과정 필요
5-1. Base64와 metadata 분리
const parts = dataURL.split(",");
- , 기준으로 나눔
- parts[0] -> data:image/png;base64
- parts[1] -> 실제 Base64 데이터만
5-2. MIME 타입 추출
const mime = parts[0].match(/:(.*?);/)![1];
- 정규식으로 image/png 부분만 추출함
- Blob 생성 시 MIME 타입을 다시 넣어야 브라우저가 png 파일로 해석 가능
5-3. Base64 문자열을 바이너리로 디코딩
const binary = stob(parts[1]);
- atob()는 브라우저 내장 함수
- Base64 문자열을 바이너리 문자열로 변환함
5-4. 바이너리 문자열의 길이 계산
const len = binary.length;
- 이 길이만큼의 실제 바이트 배열을 만들어야하기 때문에 필요
5-5. 바이트 배열(Unit8Array) 생성
const arr = new Unit8Array(len);
- 각 요소는 0 ~ 255 사이의 값
- 실제 PNG/JPG 같은 파일은 이런 바이트들의 모음이므로
- 브라우저에서 가장 적합한 바이너리 저장 방식
5-6. 바이너리 문자열 -> 바이트로 하나씩 변환
for (let i = 0; i < len; i++) {
arr[i] = binary.charCodeAt(i);
}
- 브라우저의 바이너리 문자열은 각 문자가 실제 바이트 값을 가지고 있음
최종적으로
Unit8Array [137, 80, 78, 71, ...]
완전히 PNG 바이트 배열이 됨
5-7. Blob으로 변환
return new Blob([arr], { type: mime });
- Unit8Array를 Blob에 넣으면
- 브라우저가 파일처럼 다룰 수 있는 바이너리 파일 객체가 됨
- [arr] -> 파일 데이터
- { type: mime } -> MIME 타입 (image/png 또는 image/jpeg)
1. Blob과 File 차이
1-1. Blob
- 브라우저에서 다루는 순수 바이너리 데이터 덩어리
- 이름 없음
- 수정 불가
- MIME 타입 있음
- 파일이 아니어도 됨
- Canvas -> Blob, Fetch로 받은 이미지 -> Blob, Base64 -> Blob 변환
1-2. File (Blob + 파일 정보)
- Blob을 상속한 형태로 추가 정보 가짐
- 파일 이름
- 마지막 수정 시간
- 파일 경로
const file = new File([blob], "photo.png", { type: "image/png" });
2. Unit8Array가 필요한 이유
2-1. Unit8Array란?
- 0~255 사이의 숫자를 담는 바이트 배열
- 실제 파일(이미지, 오디어 PDF)는 바이트 배열로 이루어짐
// PNG 파일의 첫 8바이트
137 80 78 71 13 10 26 10
2-2. Base64 -> Blob 변환 시 반드시 필요
- Base64를 디코딩하면 바이너리 문자열이 나옴
- 이걸 Blob으로 만들려면 1바이트씩 숫자로 변환해서 Unit8Array에 넣어야 함
- Blob은 내부적으로 Unit8Array 같은 TypedArray를 받아서 파일로 변환함
3. Blob URL이 무엇인지
- 브라우저가 Blob을 임시 파일처럼 접근할 수 있도록 하는 가짜 주소
blob:https://myapp.com/ffae13c0-2b77-4e4c-a5dc-55c01b514d5d
- 이 URL을 <img>에 넣으면 실제 이미지처럼 보임
<img src="blob:...">
- 그리고 다운로드도 가능
a.href = URL.createObjectURL(blob);
4. Base64가 어떻게 인코딩되는지
- 3바이트를 4개의 문자(6비트씩)로 바꾸는 방식
3바이트를 24비트로 합쳐서
01001001 01001110 01001110
이걸 6비트씩 4등분
010010 010100 111001 001110
각 6비트를 0~63 숫자로 정수로 변환 -> Base64 인덱스 테이블에서 문자로 매핑
'Rust' 카테고리의 다른 글
| [트러블슈팅]next.js16과 tailwindcss v4 반응형 웹 오류 (3) | 2025.11.22 |
|---|---|
| 트러블 슈팅 (1) | 2025.11.21 |
| 대비 만들기 (0) | 2025.11.18 |
| 밝기 조절하기 (0) | 2025.11.18 |
| 파일 업로드 & 흑백 필터 적용 (0) | 2025.11.14 |