본문 바로가기
Rust

이미지 다운로드하기

by 쪼꼬에몽 2025. 11. 20.

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