본문 바로가기

React

React에서 이미지 압축 기능 구현하기

이미지 업로드 전 브라우저에서 압축하면 전송량과 서버 부하를 크게 줄여 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 대응까지 구성하면 실무에서 안정적으로 운영할 수 있습니다.