2026년 5월 18일

Canvas API가 브라우저 이미지 처리를 가능하게 하는 원리

이미지를 웹 도구에 드래그했더니 서버를 거치지 않고 압축된 WebP가 돌아온다면, Canvas API가 그 일을 하고 있는 겁니다. 파일 입력부터 픽셀 조작, 블롭 출력까지 실제 동작 방식을 이해하면 이미지 라이브러리를 더 잘 활용하고, 문제가 생겼을 때 원인을 더 빠르게 찾을 수 있습니다.

파일 입력에서 픽셀 데이터까지

파이프라인은 `<input type="file">`이나 드래그앤드롭 이벤트에서 시작합니다. 브라우저는 `Blob`의 하위 타입인 `File` 객체를 건네줍니다. 픽셀을 꺼내려면 Canvas API가 그릴 수 있는 형태, 즉 `HTMLImageElement` 또는 `ImageBitmap`으로 변환해야 합니다. `FileReader` API나 `URL.createObjectURL()`이 데이터 URL 또는 객체 URL을 만들어주고, 이를 `img.src`에 할당합니다. 이미지가 `load` 이벤트를 발생시키면 그릴 준비가 된 것입니다.

'파일 선택됨'과 '그릴 준비됨' 사이의 간격은 비동기적이며, 이미지 처리 코드에서 버그가 자주 발생하는 지점입니다. `drawImage()`를 호출하기 전에 이미지 요소가 완전히 로드되어야 합니다. `load` 이벤트 전에 `drawImage()`를 호출하면 에러 없이 빈 캔버스가 나옵니다. 올바른 패턴은 항상 `img.onload` 콜백 내에서 그리기를 호출하거나, 디코딩이 완료될 때만 리졸브되는 `createImageBitmap(file)` 프로미스를 await하는 것입니다.

`createImageBitmap()`은 img 요소 방식의 현대적 대안입니다. 이미지를 메인 스레드 외부에서 디코딩하고(지원하는 브라우저에서), 전송 가능한 `ImageBitmap` 객체를 반환하며, `URL.createObjectURL()`의 URL 수명 주기 관리를 피할 수 있습니다. 이미지를 순차적으로 많이 처리하는 도구라면 Web Worker와 함께 `createImageBitmap()`을 쓰는 것이 img 요소 패턴보다 응답성이 훨씬 좋습니다.

drawImage와 비트맵 파이프라인

`drawImage(source, dx, dy, dw, dh)`는 캔버스 기반 크기 조정의 핵심입니다. 이미지 소스와 캔버스의 대상 영역을 전달합니다. `dw`와 `dh`가 소스 크기와 다르면 브라우저가 내부 보간 알고리즘으로 이미지를 스케일합니다. 기본적으로 대부분의 브라우저는 빠르지만 큰 축소에서 흐릿한 결과를 낼 수 있는 쌍선형 보간을 씁니다. `ctx.imageSmoothingQuality = 'high'`를 설정하면 더 선명한 쌍입방 보간을 요청할 수 있습니다.

고품질 축소—예를 들어 4000px 이미지를 400px로 줄이는 경우—에서는 단일 `drawImage()` 호출보다 단계별 축소가 더 나은 결과를 냅니다. 먼저 2000px로, 그다음 1000px로, 마지막에 400px로 그리는 방식입니다. 각 단계가 직접 목표 크기로 점프하는 것보다 더 많은 디테일을 보존합니다. 이는 이미지 처리 라이브러리인 Pillow의 LANCZOS 리샘플링과 동일한 기법을 캔버스 반복 연산으로 근사한 것입니다.

캔버스의 `width`와 `height` 속성은 CSS 크기와 무관하게 드로잉 서피스 해상도를 설정합니다. CSS에서 200×200px인 캔버스 요소에 `width=800 height=800`이 설정되어 있으면 내부적으로 800×800 해상도로 그립니다. 이 차이를 잘못 이해하면 흐릿한 출력(너무 작음) 또는 메모리 낭비(너무 큼)가 발생합니다. `drawImage()`를 호출하기 전에 항상 캔버스 크기를 원하는 정확한 픽셀 출력에 맞게 설정하세요.

toBlob과 재인코딩

`canvas.toBlob(callback, type, quality)`는 캔버스에서 압축 이미지 파일을 꺼내는 방법입니다. `type`은 MIME 타입: `'image/jpeg'`, `'image/webp'`, `'image/png'`. `quality`는 0에서 1 사이의 부동소수점으로, 1이 최고 품질입니다(JPEG와 WebP에만 적용되며 PNG는 항상 무손실입니다). 콜백은 `Blob`을 받아 다운로드 링크 생성이나 서버 업로드에 사용할 수 있습니다.

quality 파라미터는 커맨드라인 도구의 품질 척도와 대략적으로만 대응합니다. `toBlob()`의 `quality: 0.8`이 ImageMagick의 `quality 80`과 같지 않습니다. 각 브라우저가 자체 인코딩 테이블을 구현합니다. Safari, Chrome, Firefox의 WebP 인코더는 같은 `quality` 값에서 서로 다른 파일 크기를 만들어냅니다. 일관된 출력 크기가 중요한 도구라면 목표 파일 크기에 도달할 때까지 quality 값을 이진 탐색으로 찾아야 할 수도 있습니다.

`toDataURL()`은 `toBlob()`의 동기 대안입니다. base64 인코딩된 데이터 URL 문자열을 즉시 반환합니다. 빠른 프로토타이핑에 편리하지만 효율이 훨씬 낮습니다. base64 인코딩이 데이터를 33% 부풀리고, 동기 호출이 큰 이미지에서 메인 스레드를 차단합니다. 프로덕션 이미지 처리 도구에서는 콜백 방식의 `toBlob()`이나 프로미스화된 `canvas.convertToBlob()`(OffscreenCanvas에서 사용 가능)을 쓰세요.

성능 특성과 OffscreenCanvas

캔버스 연산은 기본적으로 메인 스레드에서 실행되므로, 큰 이미지 처리 작업은 UI를 차단합니다. 12MP 이미지를 캔버스에 그리고, 스케일하고, `toBlob()`을 호출하면 기기에 따라 200~800ms가 걸릴 수 있습니다. 그 시간 동안 페이지가 응답하지 않습니다. 스크롤 이벤트가 발생하지 않고, 애니메이션이 멈추고, 버튼 클릭이 대기열에 쌓입니다. 모바일 기기에서는 사용자가 명확히 느낍니다.

`OffscreenCanvas`는 캔버스 컨텍스트를 메인 스레드 밖으로 옮깁니다. Web Worker에서 `OffscreenCanvas`를 생성하고, 모든 그리기와 인코딩을 거기서 수행한 다음, 결과 `Blob`을 `postMessage()`로 메인 스레드에 전달합니다. Worker 스레드가 독립적으로 실행되므로 메인 스레드는 내내 응답 상태를 유지합니다. API는 일반 캔버스와 같습니다—`ctx.drawImage()`, `ctx.toBlob()`—단, OffscreenCanvas의 `toBlob()`은 콜백 대신 Promise를 반환합니다.

OffscreenCanvas는 Chrome, Firefox, Edge에서 지원됩니다. Safari는 버전 16.4(2023년)에서 지원을 추가했습니다. 최신 브라우저 전체를 대상으로 하는 도구라면 이제 OffscreenCanvas는 안전한 패턴입니다. 전송 가능한 `ImageBitmap` 객체와 잘 어울립니다. Worker에서 `createImageBitmap()`으로 디코딩하고, OffscreenCanvas에 그리고, 압축된 Blob을 반환하면 전체 파이프라인이 메인 스레드 밖에서 실행됩니다.

한계: 최대 캔버스 크기와 메모리

모든 브라우저는 최대 캔버스 크기를 제한합니다. iOS의 Safari는 구형 기기에서 캔버스를 4096×4096 픽셀(약 1670만 화소)로, 최신 기기에서는 8192×8192로 제한합니다. 데스크톱 Chrome은 32768×32768까지 허용하지만 메모리 압박으로 인해 훨씬 작은 크기에서 캔버스 연산이 자동으로 실패할 수 있습니다. 브라우저 제한을 초과하는 캔버스는 에러가 아닙니다. 단지 빈 출력을 만들 뿐이어서 디버깅하기 어려운 무성 실패입니다.

메모리 소비가 더 현실적인 제약입니다. 4000×4000 캔버스는 4000 × 4000 × 4바이트(RGBA) = 64MB의 픽셀 데이터를 RAM에 저장합니다. 이전 캔버스를 해제하지 않고 여러 이미지를 순차적으로 처리하면 메모리가 누적됩니다. 브라우저는 참조되지 않은 캔버스를 가비지 컬렉션하지만 타이밍이 예측 불가능합니다. 배치 처리 시에는 각 연산 후 `canvas.width = 0`을 설정해 GC를 기다리지 않고 즉시 픽셀 버퍼를 해제하세요.

`getImageData()`와 `putImageData()`를 통한 픽셀 연산은 비용이 큽니다. GPU에서 CPU로 픽셀 데이터를 읽어오는 `getImageData()`는 그래픽 파이프라인을 지연시킬 수 있는 동기적 GPU 리드백입니다. 크기 조정, 포맷 변환처럼 픽셀별 조작이 필요 없는 연산은 GPU 경로를 유지하는 `drawImage()`와 `toBlob()`만 사용하세요. 개별 픽셀을 실제로 검사하거나 수정해야 할 때만 `getImageData()`를 사용하세요.

Web Worker를 사용해야 할 때

이미지 처리 연산이 약 50ms 이상 걸리는 경우에는 Worker를 사용하세요. 50ms가 사용자가 UI 인터랙션에서 지연을 느끼기 시작하는 임계값입니다. 작은 JPG의 단순 압축은 20ms 안에 끝날 수 있어 메인 스레드에서 해도 무방합니다. 10MP 사진 크기 조정, 필터 적용, WebP 인코딩은 500ms가 걸릴 수 있으므로 Worker에서 처리하세요.

이미지 처리의 Worker 메시징 패턴: 메인 스레드가 `File` 또는 `ArrayBuffer`를 `postMessage()`로 Worker에 전송(복사 없이 전달하려면 transfer 사용)하고, Worker는 `createImageBitmap()`을 호출해 `OffscreenCanvas`에 그리고 `convertToBlob()`을 호출한 다음 결과 `Blob`을 반환합니다. 메인 스레드는 Blob을 받아 다운로드 URL을 만듭니다. 전체 왕복이 논블로킹입니다.

실용적인 한계 하나: Worker는 DOM이나 `window`에 접근할 수 없습니다. `fetch`, `crypto`, `indexedDB`, 그리고 `OffscreenCanvas`를 통한 Canvas API에는 접근 가능합니다. 이미지 처리가 DOM 측정(출력 크기를 결정하기 위한 요소 크기 쿼리 등)에 의존한다면, 해당 측정은 메인 스레드에서 먼저 하고 그 결과를 Worker에 전달해야 합니다.

알아두어야 할 브라우저 별 주의사항

Safari의 `toBlob('image/webp')` WebP 인코더는 Safari 16 이전에는 동작하지 않거나 미지원 상태였습니다. 이전 버전의 Safari는 WebP를 요청해도 자동으로 PNG로 폴백했습니다. 구형 iOS 사용자를 지원해야 하는 도구라면 WebP 인코딩 지원을 기능 감지한 뒤 옵션으로 제공하세요.

크로스 오리진 이미지는 캔버스를 오염시킵니다. 적절한 CORS 헤더 없이 다른 도메인의 이미지를 그리면 캔버스가 '오염'됩니다. 그리기는 가능하지만 `toBlob()`과 `getImageData()`가 SecurityError를 던집니다. 해결책은 서버 측에 있습니다. 이미지 응답에 `Access-Control-Allow-Origin: *`을 추가하고, `img.src`를 설정하기 전에 `img.crossOrigin = 'anonymous'`를 설정하세요. 둘 다 없으면 해당 세션에서 캔버스가 영구적으로 오염됩니다.

색 공간 처리는 점점 중요해지는 고려사항입니다. HDR 이미지와 광색역 색 공간(Display P3, Rec. 2020)은 표준 sRGB 캔버스에 그릴 때 색상 변이가 발생할 수 있습니다. Canvas Color Space API(`getContext('2d', { colorSpace: 'display-p3' })`)가 이를 해결하지만 브라우저 간 지원과 동작이 다릅니다. 일반적인 웹 이미지를 처리하는 도구라면 sRGB로 충분합니다. 광색역 색 공간으로 촬영하는 전문 사진가를 위한 도구를 만든다면 이 문제를 인식하고 있어야 합니다.

자주 묻는 질문

캔버스 출력이 흐릿하게 나오는 이유는 무엇인가요?

세 가지 흔한 원인이 있습니다. 캔버스 `width`/`height` 속성이 CSS 표시 크기보다 작게 설정되어 낮은 해상도로 그린 후 CSS에서 확대되는 경우, `imageSmoothingQuality`가 'low'로 설정된 경우, 또는 한 번에 크게 축소하는 경우입니다. 큰 축소에는 단계별 스케일링을 사용하고, HiDPI 디스플레이에서는 캔버스 크기에 `window.devicePixelRatio`를 곱하세요.

브라우저에서 이미지를 WebP로 변환하려면 어떻게 하나요?

`drawImage()`로 이미지를 캔버스에 그린 다음 `canvas.toBlob(callback, 'image/webp', 0.8)`을 호출하세요. 콜백은 WebP 형식의 Blob을 받습니다. Safari 16 이전 버전은 자동으로 PNG로 폴백합니다. WebP를 출력 옵션으로 제공하기 전에 작은 테스트 캔버스로 지원 여부를 감지하세요.

Canvas로 처리할 수 있는 최대 이미지 크기는 얼마인가요?

브라우저와 기기에 따라 다릅니다. iOS Safari는 구형 기기에서 4096×4096으로 제한합니다. 데스크톱 Chrome은 더 크게 허용하지만 이론적 최대값 이전에 메모리 압박이 옵니다. 실용적으로는 넓은 호환성을 위해 8000×8000을 안전한 상한으로 보세요. 설정 후 `canvas.width`를 확인하세요. 브라우저가 자동으로 clamp하면 출력이 비거나 잘릴 수 있습니다.

캔버스 이미지 처리는 안전한가요? 이미지 데이터가 브라우저 밖으로 나가나요?

모든 Canvas API 연산은 완전히 클라이언트 사이드입니다. fetch나 폼 제출 등으로 명시적으로 전송하지 않는 한 이미지 데이터는 브라우저를 떠나지 않습니다. 이것이 클라이언트 사이드 처리의 핵심 프라이버시 장점입니다. 서버가 원본 이미지를 보지 않습니다.