🌟 browser-image-compression 을 사용하게 된 이유
내가 맡은 부분이었던 메인 사진 저장과 갤러리 사진 저장하는 로직은
BASE64저장 => supabase storage저장 public Url을 받아와 useFormContext()에 저장하는 방법으로 진행되고 있었다.
사진을 BASE 64저장하는 방식에서 스토리지로 옮겨 데이터베이스 저장용량에 대한 부담은 줄었지만 storage에 저장하고 다시 public Url을 저장해 그것을 불러와 미리보기에서 보여주는데 꽤 오랜 시간이 걸렸다.
이 점을 개선하여 시간 단축을 위해 이미지를 압축하여 저장하려고 한다.
가장 널리 사용되는 라이브러리 중 하나인 browser-image-compression
을 사용하려고 한다.
이 라이브러리를 사용하면 사용자가 업로드한 이미지를 압축하여 최적화된 상태로 Supabase와 같은 저장소에 업로드할 수 있다.
pnpm add browser-image-compression
현재 로직은 단순히 supabase에서 storage로 publicUrl을 받아오는 방법이었다.
// MainPhotoInput.tsx
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const publicUrl = await uploadImageToSupabaseStorage(file);
if (publicUrl) {
setValue('mainPhotoInfo.imageUrl', publicUrl);
}
}
};
// src > utils > uploadImg.ts
export const uploadImageToSupabaseStorage = async (file: File) => {
const fileName = generateUniqueFile();
const { data, error } = await supabase.storage.from('invitation').upload(`/main_img/${fileName}`, file);
if (error) return console.error('대표사진 업로드를 실패하였습니다.', error);
const { data: urlData } = supabase.storage.from('invitation').getPublicUrl(data.path);
return urlData.publicUrl;
};
export const uploadGalleryImageToSupabaseStorage = async (file: File) => {
const fileName = generateUniqueFile();
const { data, error } = await supabase.storage.from('invitation').upload(`/gallery/${fileName}`, file);
if (error) return console.error('갤러리 업로드를 실패하였습니다.', error);
const { data: urlData } = supabase.storage.from('invitation').getPublicUrl(data.path);
return urlData.publicUrl as unknown as string;
};
// src > utils > compressImg.ts
import imageCompression from 'browser-image-compression';
export const compressImage = async (file: File) => {
const options = {
maxSizeMB: 1, // 최대 파일 크기를 1MB로 설정
maxWidthOrHeight: 800, // 최대 너비 또는 높이를 800px로 설정
useWebWorker: true, // 성능 향상을 위한 Web Worker 사용
initialQuality: 0.9, // 초기 압축 품질 (0~1)
};
try {
const compressedFile = await imageCompression(file, options);
return compressedFile;
} catch (error) {
console.error('이미지 압축 오류:', error);
return file;
}
};
1. maxSizeMB 옵션
너무 낮은 값으로 설정하면 화질이 떨어질 수 있다. 일반적으로 1MB 또는 0.5MB로 설정하면 무난
2. maxWidthOrHeight 옵션
이미지의 최대 너비 또는 높이를 제한
일반적인 웹 용도로는 800~1200px 정도면 충분
원본 이미지의 해상도가 높을수록 큰 값을 사용
3. initialQuality 옵션
기본적으로 browser-image-compression은 90% 품질로 이미지를 압축한다.
initialQuality 값을 0.8~0.9로 설정하면 화질을 크게 저하시키지 않으면서 용량을 줄일 수 있다.
한 번만 압축하여 저장한 후 보여주는 로직이 속도개선에 큰 차이를 느끼지 못하였다.
따라서 2번 압축을 하여 우리는 완성된 청첩장은 미리보기에서 SSG로 제공을 하고 있기 때문에 청첩장 제작페이지에서의 미리보기에서는 압축 미리보기를 제공하고 완성된 청첩장에는 원본 파일을 올려줘 효율성의 극대화를 결정했다.
내가 갤러리와 이미지를 넣어주는 부분과 미리보기를 해주는 컴포넌트가 달라서 zustand로 정보를 받아오기로 결정
export const compressImageTwice = async (file: File) => {
const firstCompressionOptions = {
maxSizeMB: 1,
maxWidthOrHeight: 1200,
useWebWorker: true,
initialQuality: 0.8,
};
try {
const firstCompressedFile = await imageCompression(file, firstCompressionOptions);
console.log('첫 번째 압축 후 파일 크기:', firstCompressedFile.size / 1024 / 1024, 'MB');
const secondCompressionOptions = {
maxSizeMB: 0.8,
maxWidthOrHeight: 800,
useWebWorker: true,
initialQuality: 0.7,
};
const secondCompressedFile = await imageCompression(firstCompressedFile, secondCompressionOptions);
console.log('두 번째 압축 후 파일 크기:', secondCompressedFile.size / 1024 / 1024, 'MB');
return secondCompressedFile;
} catch (error) {
throw new Error();
return file;
}
};
store > useImagePreviewStore.ts
import { create } from 'zustand';
interface MainImagePreviewState {
previewUrl: string | null;
setPreviewUrl: (url: string) => void;
}
export const useMainImagePreviewStore = create<MainImagePreviewState>((set) => ({
previewUrl: null,
setPreviewUrl: (url: string) => set({ previewUrl: url }),
}));
export const useGalleryImagePreviewStore = create<MainImagePreviewState>((set) => ({
previewUrl: null,
setPreviewUrl: (url: string) => set({ previewUrl: url }),
}));
Input부분에서 압축 파일을 zustand에 저장
// MainPhotoInput.tsx
import { useMainImagePreviewStore } from '@/store/useImagePreviewStore';
const MainPhotoInput = () => {
const { register, setValue } = useFormContext();
const introduceContent = useWatch({ name: 'mainPhotoInfo.introduceContent' });
const { setPreviewUrl } = useMainImagePreviewStore();
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const compressedFile = await compressImageTwice(file);
const compressedUrl = URL.createObjectURL(compressedFile);
setPreviewUrl(compressedUrl);
const publicUrl = await uploadImageToSupabaseStorage(file);
if (publicUrl) {
setValue('mainPhotoInfo.imageUrl', publicUrl);
}
}
};
~~~
우리는 청첩장 완성 미리보기와 제작 미리보기를 같은 컴포넌트를 사요하고 있었다.
따라서 path
를 가져와
create/card(제작 페이지)
= 압축 파일
card/
= 원본 파일을 가져와
Image src 에 넣어주었다.
// MainPhoto.tsx
const { previewUrl } = useMainImagePreviewStore();
useEffect(() => {
// 이미지 URL을 조건에 맞게 업데이트
if (path === '/create/card') {
setImageUrl(previewUrl || mainPhotoInfo.imageUrl); // previewUrl 우선, 없으면 mainPhotoInfo.imageUrl
}
}, [previewUrl, mainPhotoInfo.imageUrl, path]); // path와 imageUrl, previewUrl이 바뀔 때마다 실행
console.log(imageUrl); // 이미지 URL 업데이트 확인
return
{!imageUrl ? (
<p className='text-gray-500 w-[375px] h-[728px] bg-gray text-center'>이미지가 업로드되지 않았습니다.</p>
) : (
<div
ref={ref}
className='flex justify-center items-center relative w-full h-[600px] mb-[24px]'
onDrop={preventDefaultBehaviour}
onDragOver={preventDefaultBehaviour}
>
<Image
src={imageUrl}
alt='mainImg'
objectFit='cover'
fill
className='z-0'
/>![](https://velog.velcdn.com/images/alice0751/post/545d485b-08fd-405f-8a2a-466b0f7057a9/image.gif)