브라우저에서 사진 줄이기, 파일 크기보다 픽셀 수를 봐야 하는 이유
사진을 브라우저에서 줄여 올리면 전송량은 줄지만, 처리하는 동안에는 해상도만큼의 픽셀 배열이 메모리에 펼쳐진다.
디코딩이 무엇인지, 여러 장을 한꺼번에 줄이면 왜 화면이 멈추는지 설명한다.
이 글의 목차압축 파일과 디코딩된 픽셀6
프로필 사진이나 상품 사진을 올리는 화면을 만들다 보면, 업로드 전에 브라우저에서 사진을 줄여 보내라는 조언을 자주 만난다.
보낼 데이터가 줄면 느린 네트워크에서도 빨리 올라가기 때문이다.
그런데 휴대폰으로 찍은 사진 여러 장을 한꺼번에 고르면, 업로드가 시작되기도 전에 화면이 굳어 버리는 일이 생긴다.
파일 크기만 보면 별것 아닌데 왜 그럴까.
브라우저가 실제로 다루는 데이터는 저장된 파일과 크기가 전혀 다르다.
압축 파일과 디코딩된 픽셀
JPEG, WebP, HEIC 같은 이미지 형식은 비슷한 색과 무늬를 묶어 저장해서 바이트 수를 줄인다.
이렇게 줄인 파일은 그대로는 화면에 그릴 수 없다.
브라우저는 그리기 전에 압축을 풀어 픽셀마다 색 값을 하나씩 담은 배열로 되돌린다.
이 과정을 디코딩이라고 하고, 그렇게 나온 픽셀 배열을 비트맵이라고 한다.
기본 형식인 8비트 RGBA에서는 픽셀 하나에 빨강, 초록, 파랑, 투명도 값을 1바이트씩 담는다.
그래서 비트맵 하나의 크기는 가로 픽셀 수 × 세로 픽셀 수 × 4바이트다.
가로 4,000, 세로 3,000 픽셀 사진이라면 1,200만 픽셀이고, 비트맵은 4,800만 바이트, 약 48MB가 된다.
저장된 파일이 몇 MB든 RGBA 비트맵으로 펼치면 이 크기다.
가로와 세로를 각각 두 배로 늘리면 한 줄의 픽셀도 두 배, 줄 수도 두 배라서 전체 픽셀은 네 배가 된다.
해상도에 따라 펼쳐진 픽셀 배열의 크기가 달라진다.
전체 픽셀 750,000개
픽셀 배열 하나 3 MB
1,000 × 750은 75만 픽셀, 약 3 MB다. 가로·세로를 두 배로 키운 2,000 × 1,500은 300만 픽셀, 약 12 MB가 된다.
1,000 × 750 × 4바이트 = 3 MB다.
8비트 RGBA의 픽셀당 4바이트, 1 MB는 100만 바이트로 계산한다. 압축 파일 크기나 브라우저 전체 메모리 사용량이 아니다.
줄이는 동안 함께 올라가는 것들
사진 한 장을 줄이는 동안에는 여러 데이터가 잠깐 메모리에 함께 있다.
원본 파일, 디코딩한 비트맵, 줄인 그림을 그릴 Canvas, 다시 압축 중인 결과 파일이다.
Canvas는 브라우저 안에서 그림을 그리고 픽셀을 다루는 영역이다.
이 묶음이 사진 수만큼 겹친다.
열 장을 동시에 처리하면 큰 비트맵 열 개가 한꺼번에 메모리에 올라갈 수 있다.
그래도 브라우저에서 줄이는 이유
원본을 그대로 올리고 서버에서 줄이는 방법도 있다.
결과를 한곳에서 관리하기는 쉽지만, 사용자는 큰 파일 전체가 올라갈 때까지 기다려야 한다.
브라우저에는 이 일을 돕는 기능이 있다.
createImageBitmap()은 이미지를 비동기로 비트맵으로 만들면서 크기를 바꿀 수 있고, Canvas의 toBlob()은 그린 결과를 다시 이미지 파일로 만든다.
Blob은 브라우저가 이미지처럼 큰 이진 데이터를 파일처럼 다루는 객체다.
toBlob()에 지정한 형식을 브라우저가 지원하지 않으면 결과는 PNG로 만들어진다.
그래서 결과가 실제로 어떤 형식으로 나왔는지 한 번 확인해 두는 편이 안전하다.
목표 크기를 작게 정해도, 원본을 전체 해상도로 먼저 디코딩하는 경로라면 순간 메모리는 여전히 클 수 있다.
이 부분은 브라우저가 실제로 어떻게 디코딩하느냐에 따라 달라진다.
형식이 좋아져도 비트맵은 그대로다
WebP는 Google이 만든 이미지 형식이다.
동영상 압축 기술인 VP8의 방식을 정지 이미지에 가져와 사진의 바이트를 줄인다.
Google은 WebP가 같은 화질에서 PNG나 JPEG보다 작은 파일을 만든다고 소개한다.
그런데 줄어드는 것은 저장된 파일이다.
같은 4,000 × 3,000 사진이면 JPEG든 WebP든 RGBA 비트맵으로 펼쳤을 때 똑같이 약 48MB다.
파일 크기가 얼마나 줄지도 사진마다 다르다.
질감이 복잡한 사진, 이미 강하게 압축된 파일, 글자가 많은 캡처는 같은 설정에서도 결과가 제각각이다.
프로필 사진과 상품 사진, 인쇄용 원본은 필요한 해상도부터 다르다.
그래서 남이 쓴 크기나 품질 값을 그대로 가져오기보다, 화면에 필요한 품질을 먼저 정하고 결과가 맞는지 확인한다.
값을 바꾸면 함께 달라지는 것이 있다.
| 조정하는 값 | 줄어드는 것 | 대신 고려할 점 |
|---|---|---|
| 목표 해상도를 낮춘다 | 출력 픽셀 수와 결과 파일 크기 | 원본을 디코딩하는 메모리는 남을 수 있다 |
| 압축 품질을 낮춘다 | 결과 파일의 바이트 | 복잡한 질감이나 글자의 화질 |
| 한 번에 처리할 장수를 줄인다 | 동시에 올라가는 비트맵 | 전체 작업에 걸리는 시간 |
화면이 굳는 건 메인 스레드 때문이다
브라우저는 클릭 같은 입력 처리와 화면 그리기를 메인 스레드라는 하나의 실행 흐름에서 한다.
페이지의 자바스크립트도 기본적으로 이 스레드에서 돌기 때문에, 오래 걸리는 작업은 페이지를 반응하지 않게 만든다.
사진 변환을 메인 스레드에서 오래 돌리면 그동안 버튼이 눌리지 않고 화면이 멈춘 것처럼 보인다.
웹 워커는 무거운 계산을 별도 스레드로 옮겨 돌리는 기능이고, OffscreenCanvas를 쓰면 워커 안에서도 그림을 그려 파일로 만들 수 있다.
헷갈리기 쉬운 점이 하나 있다.
워커로 옮기면 화면은 계속 반응하지만, 디코딩해야 할 픽셀 수는 그대로다.
메모리가 모자란 문제는 워커가 풀어 주지 않는다.
여러 장을 다룰 때 챙길 것
여러 장은 한꺼번에 돌리지 않고 한두 장씩 차례로 처리한다.
시간은 조금 더 걸려도 메모리에 동시에 올라가는 비트맵 수가 줄어든다.
다 쓴 데이터는 바로 놓아준다.
ImageBitmap의 close()는 비트맵이 잡고 있던 그래픽 자원을 정리한다.
미리보기용으로 만든 주소는 URL.revokeObjectURL()로 해제해야 브라우저가 그 파일을 계속 붙잡고 있지 않다.
변환이 실패할 때의 길도 만들어 둔다.
메모리가 모자라 실패하는 상황은 기기와 사진에 따라 달라서, 메모리가 넉넉한 개발용 컴퓨터에서는 잘 드러나지 않는다.
실패하면 원본을 그대로 올리거나 다시 고르도록 안내하면 된다.
브라우저에서 줄이는 것은 사용자 경험을 위한 최적화다.
누군가는 화면을 거치지 않고 서버로 바로 파일을 보낼 수 있다.
요청에 담겨 오는 파일 형식 정보도 쉽게 위조할 수 있다.
그래서 형식과 크기는 서버에서 다시 확인한다.