Posts

canvas.toBlob이 IOS에서 null을 반환 받는 경우

2024-11-12

회사에서 react-easy-crop 라이브러리와 Canvas API를 쓰다가, iOS에서만 zoom이 1 이하인 경우 canvas.toBlob이 null을 반환하는 현상을 발견했어요. 안드로이드와 데스크톱에서는 아무 문제가 없었는데, iOS에서만 재현되는 버그라 처음엔 원인을 찾기가 쉽지 않았어요.

iOS Canvas의 픽셀 수 제한

결론부터 말하면, iOS WebKit은 Canvas 픽셀 수에 상한선을 두고 있어요. 제한 수치는 디바이스마다 다른데, 일반적으로 약 16,777,216픽셀(4096×4096)을 초과하면 toBlob()이 null을 반환해요. 예외를 던지거나 오류 메시지를 주는 게 아니라 그냥 콜백에 null을 넘겨주거든요. 처음 보면 왜 null이 오는지 전혀 감이 잡히지 않아요.

이 제한은 iOS가 GPU 메모리를 엄격하게 관리하기 때문에 생기는 거예요. 데스크톱 브라우저나 Android는 OS 수준에서 메모리 사용에 상대적으로 너그럽지만, iOS는 앱 하나가 쓸 수 있는 GPU 메모리를 강하게 제한해요. Canvas 내용은 CPU 메모리가 아니라 GPU 텍스처로 올라가니까, 이 한도를 넘어서면 브라우저가 그냥 조용히 실패하는 거죠.

zoom이 1 이하일 때 캔버스가 왜 커지는가

zoom이 1 이하라는 건 이미지를 축소해서 보고 있다는 뜻이에요. 즉 화면에 보이는 영역보다 더 넓은 원본 이미지 영역을 canvas에 담아야 한다는 거죠. react-easy-crop은 crop 영역을 픽셀 기준으로 계산하는데, zoom이 작아질수록 같은 화면 크기 안에 더 많은 원본 픽셀이 들어가야 하니까 canvas 크기가 반비례하여 커져요. 예를 들어 zoom이 0.5이면 원본 이미지의 두 배 면적을 canvas에 그려야 하는 상황이 되는 거예요. 이 계산이 그대로 canvas.width, canvas.height에 반영되면 픽셀 수가 순식간에 한계를 넘어서요.

해결: zoom만큼 캔버스 크기를 줄이기

해상도를 무조건 낮추는 건 서비스 품질에 영향을 주니까, zoom이 1 이하일 때만 그 비율만큼 캔버스 크기를 줄이는 방식을 선택했어요.

const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
newImage.src = imagePath;
newImage.onload = () => {
  // zoom >= 1이면 원본 크기 유지, zoom < 1이면 그만큼 축소
  const scalefactor = Math.min(zoom, 1);

  canvas.width = width * scalefactor;
  canvas.height = height * scalefactor;

  // 캔버스 크기를 줄인 만큼 그리기 컨텍스트도 같은 비율로 스케일 조정
  ctx.scale(scalefactor, scalefactor);
  ctx.drawImage(newImage, 0, 0);

  canvas.toBlob((blob) => {
    // ...로직
  }, 'image/png');
};

Math.min(zoom, 1)로 zoom이 1 이상인 경우는 그대로 두고, 1 미만인 경우에만 스케일을 적용해요. 캔버스 크기를 줄인 뒤 ctx.scale()로 그리기 좌표계도 같은 비율로 맞춰주면, 이미지는 축소된 캔버스에 올바르게 그려져요. 결과적으로 zoom이 0.5일 때 절반 크기의 캔버스가 생성돼서 픽셀 수 제한을 피할 수 있고, 해상도 저하도 실질적으로는 zoom에 비례하는 수준으로 제한되는 거죠.

크로스 브라우징 이슈는 대부분 겉으로는 단순해 보이지만, 파고들면 각 플랫폼의 렌더링 정책 차이에서 비롯돼요. iOS의 Canvas 메모리 제한처럼 스펙 문서보다 실제 기기에서 먼저 만나게 되는 경우가 많아서, 한 번 겪어두면 비슷한 상황에서 훨씬 빨리 원인을 좁힐 수 있어요.