이미지 업로드 전 브라우저에서 압축하면 전송량과 서버 부하를 크게 줄여 LCP와 TTFB 개선에 도움됩니다. 실무에서는 라이브러리 방식으로 빠르게 적용하고, 캔버스 기반으로 커스텀 제어를 추가하는 전략이 효율적입니다.
1. 빠르게 적용: browser-image-compression
가장 간단한 방법은 browser-image-compression 라이브러리를 사용하는 것입니다. 전용 Web Worker를 활용해 큰 이미지도 메인 스레드 블로킹 없이 처리합니다.
설치: npm i browser-image-compression
import React, { useState } from 'react';
import imageCompression from 'browser-image-compression';
function ImageCompressor() {
const [original, setOriginal] = useState(null);
const [compressed, setCompressed] = useState(null);
const [progress, setProgress] = useState(0);
const [error, setError] = useState('');
const renameWithExt = (name, mime) => {
const ext = (mime?.split('/')[1] || 'jpg').toLowerCase();
const base = name.replace(/\.[^/.]+$/, '');
return `${base}.${ext}`;
};
const handleChange = async (e) => {
setError('');
const file = e.target.files?.[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
setError('이미지 파일만 업로드 가능합니다.');
return;
}
setOriginal({
file,
url: URL.createObjectURL(file),
sizeKB: (file.size / 1024).toFixed(1),
type: file.type
});
const options = {
// 1MB 이하 목표
maxSizeMB: 1,
// 긴 변 기준 리사이즈
maxWidthOrHeight: 1920,
useWebWorker: true,
onProgress: (p) => setProgress(p),
// 포맷 유지(기본) 또는 'image/webp'로 더 높은 압축률 선택
// fileType: 'image/webp'
};
try {
const blobOrFile = await imageCompression(file, options);
const mime = blobOrFile.type || file.type || 'image/jpeg';
const compressedFile = blobOrFile instanceof File
? blobOrFile
: new File([blobOrFile], renameWithExt(file.name, mime), { type: mime });
setCompressed({
file: compressedFile,
url: URL.createObjectURL(compressedFile),
sizeKB: (compressedFile.size / 1024).toFixed(1),
type: compressedFile.type
});
} catch (err) {
console.error(err);
setError('압축 중 오류가 발생했습니다.');
}
};
const upload = async () => {
try {
const fileToSend = compressed?.file || original?.file;
if (!fileToSend) return;
const fd = new FormData();
fd.append('image', fileToSend);
const res = await fetch('/api/upload', { method: 'POST', body: fd });
if (!res.ok) throw new Error('Upload failed');
alert('업로드 완료');
} catch (e) {
setError('업로드 실패: ' + e.message);
}
};
return (
<div>
<input type="file" accept="image/*" onChange={handleChange} />
{error && <p style={{ color: 'red' }}>{error}</p>}
{original && (
<div>
<p>원본: {original.sizeKB} KB ({original.type})</p>
<img src={original.url} alt="original preview" style={{ maxWidth: 240 }} />
</div>
)}
{progress > 0 && progress < 100 && <p>압축 진행률: {progress}%</p>}
{compressed && (
<div>
<p>압축본: {compressed.sizeKB} KB ({compressed.type})</p>
<img src={compressed.url} alt="compressed preview" style={{ maxWidth: 240 }} />
</div>
)}
<button onClick={upload}>업로드</button>
</div>
);
}
export default ImageCompressor;옵션 가이드: maxSizeMB는 목표 용량, maxWidthOrHeight는 리사이즈 기준입니다. fileType을 image/webp로 지정하면 품질 대비 크기를 더 줄일 수 있으나, 서버와 표시 환경 호환성을 고려해야 합니다.
2. 커스텀 제어: Canvas 기반 압축
캔버스 API로 포맷/품질/리사이즈를 직접 제어할 수 있습니다. EXIF 회전은 createImageBitmap 옵션으로 상당 부분 자동 처리됩니다.
export async function compressWithCanvas(file, {
maxWidth = 1920,
quality = 0.8,
mime = 'image/jpeg'
} = {}) {
// EXIF 회전 반영(지원 브라우저)
const bitmap = await createImageBitmap(file, { imageOrientation: 'from-image' });
const scale = Math.min(1, maxWidth / bitmap.width);
const width = Math.round(bitmap.width * scale);
const height = Math.round(bitmap.height * scale);
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(bitmap, 0, 0, width, height);
const blob = await new Promise((resolve) => canvas.toBlob(resolve, mime, quality));
if (!blob) throw new Error('toBlob 실패');
const ext = mime === 'image/webp' ? '.webp' : mime === 'image/png' ? '.png' : '.jpg';
const name = file.name.replace(/\.[^/.]+$/, '') + ext;
return new File([blob], name, { type: mime });
}투명도가 필요한 경우 mime을 image/png 또는 image/webp로 지정하세요. PNG는 품질 파라미터가 무시되는 점에 유의합니다.
3. 업로드 파이프라인에 연결하기
실무에서는 압축 성공 시 압축본을, 실패 시 원본을 업로드하도록 폴백을 구성합니다.
async function uploadImage(file) {
const fd = new FormData();
fd.append('image', file);
const res = await fetch('/api/upload', { method: 'POST', body: fd });
if (!res.ok) throw new Error('업로드 실패');
return res.json();
}
async function handleUpload(file) {
try {
const compressed = await compressWithCanvas(file, { maxWidth: 1920, quality: 0.8, mime: 'image/webp' });
return await uploadImage(compressed);
} catch (e) {
console.warn('압축 실패, 원본 업로드로 폴백:', e);
return await uploadImage(file);
}
}4. 품질·크기 튜닝 전략
권장 초기값: 긴 변 1920px, JPEG/WebP 품질 0.75~0.85, 목표 0.5~1MB. 썸네일은 640~1024px로 더 공격적으로 압축합니다. 사용자의 촬영 환경이 다양한 모바일에서는 EXIF 회전 처리와 메모리 사용량을 고려해 Web Worker를 적극 활용합니다.
5. 실무 팁과 엣지 케이스
HEIC/HEIF 처리: iOS 사진은 HEIC가 많습니다. 서버가 미지원이면 heic2any로 JPEG/WebP 변환을 적용하세요.
import heic2any from 'heic2any';
async function convertHeic(file) {
if (!/heic|heif/i.test(file.type)) return file;
const blob = await heic2any({ blob: file, toType: 'image/jpeg', quality: 0.8 });
return new File([blob], file.name.replace(/\.[^/.]+$/, '') + '.jpg', { type: 'image/jpeg' });
}입력 제한: input accept="image/*"로 제한하고 최대 파일 크기(예: 20MB)를 프론트에서 선제 검사합니다.
미리보기 관리: URL.createObjectURL로 생성한 URL은 컴포넌트 unmount 시 URL.revokeObjectURL로 해제해 메모리 누수를 방지합니다.
서버 협업: 서버가 WebP를 지원하면 프론트에서 WebP로 전송하고, CDN 변환(예: AVIF 변환)과 Cache-Control 설정으로 추가 최적화합니다.
6. 성능과 UX 체크리스트
메인 스레드 보호: 대용량 이미지 다중 압축 시 순차 처리 또는 큐를 사용하고, 라이브러리의 useWebWorker 옵션을 켭니다.
진행률 표시: onProgress로 사용자에게 피드백을 제공해 이탈을 줄입니다.
품질 검증: 눈에 띄는 블록 노이즈가 없는지 샘플 이미지로 QA를 진행합니다.
지표 모니터링: 이미지 축소로 업로드 속도와 LCP가 얼마나 개선되는지 사후 측정합니다.
7. 마무리
React에서의 이미지 압축은 라이브러리로 빠르게 적용하고, 캔버스 기반으로 세밀하게 제어하면 품질과 성능을 균형 있게 가져갈 수 있습니다. 단계적 폴백과 WebP/HEIC 대응까지 구성하면 실무에서 안정적으로 운영할 수 있습니다.
'React' 카테고리의 다른 글
| React에서 스크롤 기반 Parallax 효과 구현하기 (0) | 2026.06.18 |
|---|---|
| React 앱에서 OAuth 1.0 인증 처리하기 (0) | 2026.06.17 |
| React 앱에서 사용자 세션 타이머 및 자동 로그아웃 구현하기 (1) | 2026.06.17 |
| React에서 외부 스크립트 동적 로드 및 관리하기 (0) | 2026.06.16 |
| React 앱에서 브라우저 Notification API로 알림 보내기 (0) | 2026.06.16 |