사진 업로드 기능을 붙이고 나니 문제가 하나 보였다. expo-image-picker로 갤러리에서 고른 이미지를 그대로 서버로 보냈더니, 요즘 폰 카메라 원본이 한 장에 4~5MB씩 나온다. 느린 네트워크에서는 업로드가 하염없이 돌고, 스토리지 비용도 쌓인다. 정작 화면에 보여줄 땐 가로 1080px면 충분한데 4000px짜리를 통째로 올리고 있던 거다.
클라이언트에서 미리 줄여서 보내면 되는 일이었다. 이걸 해주는 게 expo-image-manipulator다.
새 API로 바뀌었다
예전엔 manipulateAsync(uri, [...], {...}) 한 방으로 썼는데, 최근 SDK부터는 컨텍스트 기반의 객체형 API로 바뀌었다. 기존 함수는 deprecated라 새로 짜는 코드는 이쪽을 쓰는 게 맞다.
npx expo install expo-image-manipulator
import { ImageManipulator, SaveFormat } from 'expo-image-manipulator';
async function shrink(uri: string) {
const context = ImageManipulator.manipulate(uri);
context.resize({ width: 1080 }); // 높이는 비율 맞춰 자동
const image = await context.renderAsync();
const result = await image.saveAsync({
format: SaveFormat.JPEG,
compress: 0.7, // 0~1, 낮을수록 더 압축
});
return result; // { uri, width, height }
}
흐름이 세 단계로 나뉜다. manipulate(uri)로 컨텍스트를 만들고, resize·rotate·crop 같은 조작을 체인으로 걸고, renderAsync() → saveAsync()로 실제 파일을 뽑는다. 조작을 여러 개 걸 때 매번 중간 파일을 안 만들고 한 번에 처리해서 메모리 측면에서도 낫다.
업로드 흐름에 끼워 넣기
image-picker에서 받은 uri를 바로 서버로 보내지 말고 shrink를 한 번 통과시킨다.
const picked = await ImagePicker.launchImageLibraryAsync({ quality: 1 });
if (picked.canceled) return;
const small = await shrink(picked.assets[0].uri);
const form = new FormData();
form.append('photo', {
uri: small.uri,
name: 'photo.jpg',
type: 'image/jpeg',
} as any);
await fetch('/api/upload', { method: 'POST', body: form });
picker 단계에서 quality를 낮추는 방법도 있지만, 그건 압축만 되고 해상도(픽셀 수)는 안 줄어든다. 전송 용량을 확 줄이려면 결국 resize로 가로 픽셀을 깎는 게 효과가 크다. 실제로 4032px 원본을 1080px·compress 0.7로 돌렸더니 4.6MB가 280KB 정도로 떨어졌다.
한 가지 팁
PNG 스크린샷처럼 투명도가 필요한 이미지가 아니라면 SaveFormat.JPEG가 용량 면에서 거의 항상 유리하다. 반대로 로고·아이콘처럼 투명 배경이 중요하면 PNG로, 더 줄이고 싶으면 WEBP로 저장하면 된다.
업로드 전에 한 줄 끼워 넣는 것만으로 네트워크 시간과 서버 비용이 같이 줄어든다. 사진을 다루는 앱이라면 picker 바로 뒤에 붙여두길 추천한다.