내가 이미지 변환기를 만든 방법, 문자 그대로 당신의 파일을 볼 수 없습니다
Source: Dev.to
번역할 텍스트를 제공해 주시면 한국어로 번역해 드리겠습니다.
이미지 업로드를 무작위 변환 사이트에 올리는 것이 지겨워졌어요
느려서가 아니라—그렇긴 했지만—그런 사이트에 파일을 드롭할 때마다 그 파일이 반대편에서 어떻게 처리되는지 전혀 알 수 없었어요. 이용 약관은 항상 세 페이지 정도 길고 정확히 필요한 부분에서 모호했죠.
“우리는 업로드된 콘텐츠를 서비스 개선에 사용할 수 있습니다.”
물론이죠.
그래서 직접 만들었고, 그 과정에서 브라우저가 대부분 개발자들이 생각하는 것보다 훨씬 더 강력하다는 것을 배웠습니다.
How it works under the hood
The problem with server‑side conversion
Most image‑converter tools follow the same architecture:
- You upload the file to their server.
- Their backend runs ImageMagick (or something similar).
- The converted file gets written to their storage.
- You download it.
- They delete it… probably.
The “probably” is the part I couldn’t get past. Even setting aside the privacy angle, this approach has a real performance problem: you’re bottlenecked by your upload speed, not your device’s actual processing capability. A modern laptop can encode a JPEG in milliseconds. Waiting for a file to travel to a server and back just to do that makes no sense.
The alternative I wanted was simple: do all of it in the browser, on the user’s own machine. No upload. No server. No wondering.
What the browser can actually do
Turns out, quite a lot.
Modern browsers expose a Canvas API that handles image encoding and decoding natively. The flow for a basic JPG → PNG conversion looks like this:
async function convertImage(file, targetFormat) {
const bitmap = await createImageBitmap(file);
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
const ctx = canvas.getContext('2d');
ctx.drawImage(bitmap, 0, 0);
const blob = await canvas.convertToBlob({
type: `image/${targetFormat}`,
quality: 0.92,
});
return blob;
}
That’s the whole thing. The file goes from the user’s disk into browser memory, gets re‑encoded, and comes back out as a download — without a single network request being made.
Tip: Open the Network tab in DevTools while converting a file; you’ll see zero upload requests. That’s not a policy, it’s a technical reality.
Where it gets more interesting: HEIC
JPG, PNG, and WebP are straightforward because browsers handle them natively. HEIC (High Efficiency Image Container) is a different story.
- HEIC is Apple’s default photo format since iOS 11.
- It offers better compression than JPG at equivalent quality and supports depth maps, Live Photos, etc.
- Windows and most web services don’t know what to do with it—upload a HEIC to half the sites on the internet and you get an error.
Browsers don’t decode HEIC natively, so I needed a different approach: WebAssembly.
import { libheif } from 'libheif-js';
async function decodeHEIC(file) {
const decoder = new libheif.HeifDecoder();
const buffer = await file.arrayBuffer();
const data = new Uint8Array(buffer);
const images = decoder.decode(data);
const image = images[0];
const width = image.get_width();
const height = image.get_height();
const pixelData = await new Promise((resolve, reject) => {
image.display(
{ data: new Uint8ClampedArray(width * height * 4), width, height },
(result) => {
if (!result) reject(new Error('HEIC decode failed'));
else resolve(result);
}
);
});
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
const imageData = new ImageData(pixelData.data, width, height);
ctx.putImageData(imageData, 0, 0);
return new Promise((resolve) =>
canvas.toBlob(resolve, 'image/jpeg', 0.92)
);
}
The WASM module (libheif-js) runs a compiled C++ HEIC decoder directly in the browser tab. It’s real image processing—decoding a proprietary Apple format—using the user’s CPU, without any server involvement.
The first time I got this working, it felt a bit absurd. You’re essentially running native code in a browser tab. But that’s exactly what WebAssembly is for.
The performance reality
One thing I didn’t expect: local processing is often faster than server‑side, not just for privacy reasons.
| Scenario | Typical time (modern machine) |
|---|---|
| Canvas‑based JPG → PNG (3–5 MB) | 0.3–0.6 seconds |
| HEIC → JPEG (2–4 MB) | 0.8–1.5 seconds |
| Multiple files in batch (10 × 5 MB) | ~5 seconds total |
These numbers show that doing the work locally isn’t just a privacy win—it’s also a speed win. No network latency, no server queue, just raw CPU/GPU power at your fingertips.
| 100–300 ms | | HEIC → JPEG via WASM (large iPhone photo) | 1–3 s | | Server‑side (upload + process + download) | > 3 s (depends on connection) |
서버‑사이드가 유리한 경우는 많은 대용량 파일을 배치 처리할 때이며, 서버 하드웨어 전반에 걸친 병렬 처리가 중요합니다. 단일 변환에서는 로컬이 더 빠릅니다.
브라우저 기능에 대해 배운 점
이 작업을 만들면서 서버에 무엇을 두어야 하는지에 대한 생각이 바뀌었습니다. “우리 서버에 업로드하면 처리한다”는 식으로 만들어지는 도구들이 실제로는 그렇게 필요하지 않은 경우가 많습니다. 브라우저는 다음을 제공합니다:
- 내장 코덱 지원을 갖춘 강력한 2‑D Canvas API.
- 계산 집약적인 작업을 위한 WebAssembly.
- 로컬 파일을 읽기 위한 File API와 FileReader.
- 메모리에 모두 로드하지 않고 큰 파일을 처리할 수 있는 Streams API.
파일을 서버로 보내는 반응은 종종 습관일 뿐, 필수는 아닙니다. 사용자가 이미 로컬에 파일을 가지고 있고 단순히 변환만 필요할 경우, 로컬 처리를 진지하게 고려할 가치가 있습니다.
앞으로의 방향
다음에 추가할 포맷은 AVIF입니다. WebP에 비해 압축 효율이 실제로 뛰어나며—테스트 결과 사진 콘텐츠에서 동일한 품질 기준으로 파일 크기가 20–30 % 감소합니다. 문제는 AVIF 인코딩이 WebP보다 계산량이 많다는 점이지만, 동일한 브라우저‑사이드 접근 방식(Canvas + WASM)을 사용하면 사용자의 머신을 떠나지 않고도 로컬에서 처리할 수 있습니다.
개요
WASM 접근 방식은 사용자를 기다리게 하지 않도록 보다 신중한 최적화가 필요합니다.
현재 작업
- Batch conversion 적절한 큐와 함께.
- 비정상적인 색상 프로파일에 대한 더 나은 처리 (예: 일부 경우에 예상치 못하게 동작하는 최신 iPhone의 와이드‑갬마 이미지).
Demo
이 기능을 직접 보고 싶다면 제가 만든 실시간 도구 imageconvert.website 를 확인해 보세요.
사이트에는 다음이 포함되어 있습니다:
- 변환기
- HEIC 도구
- 압축기
이 모든 구성 요소는 위에서 설명한 워크플로를 구현합니다.
구현 세부 사항에 대해 궁금한 점이 있으면 댓글로 자유롭게 질문해 주세요!