나는 당신의 이미지를 절대 보지 않는 image compressor를 만들었습니다

발행: (2026년 5월 2일 PM 04:23 GMT+9)
10 분 소요
원문: Dev.to

Source: Dev.to

위에 제공된 내용 외에 번역할 텍스트가 없습니다. 번역을 원하는 본문을 제공해 주시면 한국어로 번역해 드리겠습니다.

MiniPx – 완전 브라우저 기반 이미지 압축기

내가 사용해 본 모든 온라인 이미지 압축기는 같은 문제를 가지고 있었다: 사진을 서버에 업로드한다.

TinyPNG, iLoveIMG, Compress2Go — 모두 같은 방식이다. 파일을 선택하면 다른 사람의 컴퓨터로 전송돼 압축된 뒤 다시 돌아온다. 압축 품질은 좋지만, 사진의 GPS 좌표, 기기 일련 번호, 타임스탬프 등 EXIF 데이터에 포함된 정보가 내가 제어할 수 없는 서버에 저장된다.

나는 계속 생각했다: 이미지 압축은 단순히 수학 — Canvas API, 품질 파라미터, 블롭 조작. 서버가 필요할 이유가 없다는 것을.

그래서 MiniPx를 만들었다

이미지를 완전히 브라우저 내에서 압축, 변환, 리사이즈한다. 절대 업로드되지 않는다. 여기서부터가 그 작동 원리다.

The core compression loop

실제 압축은 약 20줄 정도의 코드로 이루어집니다. 이미지를 캔버스에 로드하고, 그린 뒤, 품질 파라미터를 지정하여 블롭으로 내보냅니다:

function compressAtQuality(img, w, h, fmt, quality) {
  return new Promise((resolve, reject) => {
    const canvas = document.createElement('canvas');
    canvas.width = w;
    canvas.height = h;
    const ctx = canvas.getContext('2d');

    // White background for JPEG (no transparency support)
    if (fmt === 'image/jpeg') {
      ctx.fillStyle = '#fff';
      ctx.fillRect(0, 0, w, h);
    }

    ctx.drawImage(img, 0, 0, w, h);
    canvas.toBlob(
      (blob) => (blob ? resolve(blob) : reject(new Error('No output'))),
      fmt,
      fmt === 'image/png' ? undefined : quality
    );
  });
}

그게 전부입니다. Sharp도, ImageMagick도, 서버‑사이드 어떤 것도 필요 없습니다. 브라우저에 내장된 JPEG/WebP 인코더가 실제 압축을 담당합니다.

Source:

아무도 이야기하지 않는 문제: 압축이 파일을 더 크게 만들 때

잘 최적화된 JPEG를 quality = 0.65 로 Canvas에 넣으면, 출력 파일이 입력보다 클 수 있습니다. 브라우저는 이미 압축된 원본을 알지 못하고 이미지를 처음부터 다시 인코딩합니다.

테스트 중에 사용자는 200 KB JPEG를 넣고 280 KB 파일을 받게 되었습니다. 이는 매우 난처한 상황이었습니다.

폴백 체인

초기 압축으로 파일이 더 커졌다면, 원본보다 작아질 때까지 낮은 품질 단계로 내려갑니다:

let blob = await compressAtQuality(img, w, h, fmt, quality);

if (blob.size >= file.size && fmt !== 'image/png') {
  for (const fallbackQ of [0.6, 0.45, 0.3, 0.2]) {
    if (fallbackQ >= quality) continue;

    const attempt = await compressAtQuality(img, w, h, fmt, fallbackQ);
    if (attempt.size = file.size && fmt === 'image/webp') {
      const jpegFallback = await compressAtQuality(
        img,
        w,
        h,
        'image/jpeg',
        Math.min(quality, 0.5)
      );
      if (jpegFallback.size  file.size * 1.5 && fmt === 'image/png') {
        const webpAlt = await compressAtQuality(img, w, h, 'image/webp', quality);
        const jpegAlt = await compressAtQuality(img, w, h, 'image/jpeg', quality);
        const smallest = [blob, webpAlt, jpegAlt].sort((a, b) => a.size - b.size)[0];
        if (smallest.size  {
          return new Promise((resolve) => {
            const img = new Image();
            img.onload = () => resolve(true);
            img.onerror = () => resolve(false);
            img.src = 'data:image/heic;base64,AAAAGGZ0eXBoZWlj';
            setTimeout(() => resolve(false), 500);
          });
        };
      }
    }
  }
}
  • Safari 사용자는 동일한 Canvas 트릭을 통해 의존성 없이 HEIC 변환을 수행합니다: HEIC를 로드하고, 캔버스에 그린 뒤 JPEG로 내보냅니다. 라이브러리가 필요 없습니다.
  • Chrome/Firefox 사용자heic2any(WASM 기반 HEIC 디코더, 약 350 KB)를 사용합니다. HEIC 파일을 실제로 변환해야 할 때만 지연 로드됩니다:
const heic2any = (await import('heic2any')).default;
return await heic2any({ blob: file, toType: 'image/jpeg', quality: 0.92 });

Safari는 이 350 KB를 절대 다운로드하지 않으며, Chrome 사용자는 HEIC 변환이 필요할 때만 다운로드합니다. 그 외 모든 사용자는 가벼운 경로를 이용합니다.

EXIF 데이터 제거 (프라이버시 부분)

휴대폰으로 촬영한 사진에는 EXIF 메타데이터가 포함됩니다: GPS 좌표, 기기 모델, 일련 번호, 타임스탬프, 때로는 이름까지도.

이미지를 Canvas를 통해 다시 그리면 EXIF 데이터가 함께 전달되지 않습니다. Canvas는 픽셀만 인식하고 메타데이터 개념이 없기 때문입니다. 따라서 MiniPx를 통과하는 모든 이미지는 깨끗하게 나옵니다: GPS도 없고, 기기 정보도 없으며, 타임스탬프도 없습니다.

“EXIF 데이터 제거” 토글이 제공되며 기본값으로 켜져 있습니다. Canvas 재인코딩이 이를 자동으로 처리하므로 추가 코드는 필요하지 않습니다.

아키텍처

MiniPx는 Next.js 15 정적 사이트입니다. API 라우트가 없으며 서버‑사이드 처리도 없습니다; 모든 작업이 클라이언트의 브라우저에서 실행됩니다.

MiniPx는 최신 브라우저가 이미 안전하고 개인적인 이미지 압축, 변환, 그리고 프라이버시를 보호하는 메타데이터 제거에 필요한 모든 기능을 갖추고 있음을 보여줍니다—서버가 전혀 필요하지 않습니다.

Overview

  • Framework: Next.js 15 (static export)
  • Hosting: Netlify (free tier) – pre‑rendered HTML + JS served from Netlify’s CDN
  • Components:
    • 5 client components: ImageTool, PDFTool, HEICTool, TrackedCTA, WebVitals
    • All other parts are server‑rendered (SEO content, schemas, navigation)
  • Dependencies: 8 total

Performance

  • First‑load JavaScript for any page: ≈ 103‑106 KB (entire app – React, compressor, UI, etc.)
  • Comparison: TinyPNG’s homepage loads 2.4 MB of JavaScript

I’m aggressive about keeping things server‑rendered. The tool pages contain long‑form SEO content, FAQ accordions, and JSON‑LD schemas, all rendered as static HTML. The only client‑side JavaScript is the actual image‑processing tool.

내가 다르게 할 점

  1. 배치 처리 속도

    • 현재 파일은 순차적으로 처리됩니다.
    • Web Workers를 사용하면 병렬 압축이 가능하지만, Canvas API는 워커에서 사용할 수 없습니다.
    • OffscreenCanvas가 존재하지만, 브라우저 지원이 아직 고르지 않습니다. 이를 지켜보고 있습니다.
  2. PNG 최적화

    • 클라이언트‑사이드 PNG 최적화는 여전히 어려운 문제입니다.
    • pngquantoxipng의 WASM 포트가 존재하지만, 번들에 **500 KB+**를 추가합니다.
    • 현재는 포맷 전환 폴백이 작동하지만, 본질적으로 임시 방편에 불과합니다.
  3. 미리보기 기능

    • 다운로드 전에 압축된 이미지의 미리보기가 없습니다.
    • 나란히 보여주는 미리보기를 추가하면 UX가 향상되지만, 이미지당 두 개의 Blob URL을 유지해야 하므로 배치 업로드 시 메모리 사용량이 크게 증가합니다.

사용해 보기

MiniPx는 무료입니다. 회원가입도 필요 없고, 제한도 없으며, 광고도 없습니다.

비슷한 것을 만들고 있다면, 핵심 통찰은: Canvas + toBlob은 서버‑사이드 이미지 처리의 약 90 %를 제공하며, 인프라 비용이 전혀 들지 않습니다.
남은 약 10 % (PNG 최적화, 비‑Safari에서의 HEIC, 고급 필터)는 WASM 라이브러리가 필요하지만, 이를 지연 로드하면 대부분의 사용자는 비용을 지불하지 않게 할 수 있습니다.

0 조회
Back to Blog

관련 글

더 보기 »