react 이미지 최적화 하기 (react-image-file-resizer)

G-NOTE·2023년 8월 27일
7

React

목록 보기
24/27

문제 상황

사이드 프로젝트 배포 후 모바일 디바이스에서 서비스를 이용할 때 게시글 생성 중 이미지 용량이 클 경우 하단 내용 입력 창 딜레이가 커지는 이슈가 발생했다.

리사이징 전 입력창 딜레이

또한 게시글 목록에서 서버에서 이미지를 받은 후 뷰포트에 그리는 시간이 길어지면서 이미지가 뚝뚝 끊기면서 로딩되는 현상도 발생했다.

리사이징 전 이미지 끊김

원인

이미지 업로드 용량을 10MB로 제한하고 있었다. 그러다 보니 10MB 이하의 고화질 이미지를 등록할 경우 화면 전체가 느려지면서 입력창 딜레이가 심해졌고, 프론트와 백엔드 모두 이미지 압축을 하고 있지 않아서 리스트를 불러올 때도 딜레이가 길었던 것이다.
따라서 프론트엔드/백엔드 모두 이미지 최적화를 하기로 했다.

해결 (1) - browser-image-compression

browser-image-compression 라이브러리를 사용하여 이미지를 리사이징하기로 했다.

선택 이유

  • 이미지 리사이징 라이브러리 중 사용자가 가장 많았음 (npm trends 비교)
  • 타 라이브러리와 비교하여 가장 최근에 업데이트가 이루어짐

code

  const handleAddImg = async (e: ChangeEvent<HTMLInputElement>) => {
    const target = e.target.files;

    if (target && target.length) {
      if (target.length + previewList.length > DISK_IMG_MAX_LENGTH) {
        // 이미지 최대 개수 제한
        window.alert(
          `사진은 최대 ${DISK_IMG_MAX_LENGTH}개까지 등록할 수 있습니다.`
        );
      } else {
        const newFiles: File[] = Array.from(target);
        newFiles.map(async (file) => {
          if (file.size > IMG_MAX_SIZE) {
            // 이미지 최대 용량 제한
            window.alert(
              `${Math.round(
                IMG_MAX_SIZE / 1000000
              )}MB 이하의 사진만 등록할 수 있습니다.`
            );
          } else {
            const options = {
              maxSizeMB: 2, // 이미지 최대 용량
              maxWidthOrHeight: 900, // 최대 넓이(혹은 높이)
              fileType: "image/jpeg",
              useWebWorker: true,
            };
            const previewOptions = {
              maxSizeMB: 0.3, // 이미지 최대 용량
              maxWidthOrHeight: 900, // 최대 넓이(혹은 높이)
              fileType: "image/jpeg",
              useWebWorker: true,
            };
            try {
              const compressedBlob = await imageCompression(file, options);
              const compressedFile = new File([compressedBlob], file.name, {
                type: file.type,
              });
              setFiles((prev) => [...prev, compressedFile]);

              const compressedPreview = await imageCompression(
                file,
                previewOptions
              );
              const reader = new FileReader();
              reader.readAsDataURL(compressedPreview);
              reader.onloadend = () => {
                const previewImgUrl = reader.result;
                setPreviewList((prev: DiskImgType[]) => [
                  ...prev,
                  { imgId: "new", imgUrl: previewImgUrl as string },
                ]);
                if (!previewList.length) {
                  setMainImg(previewImgUrl as string);
                }
              };
            } catch (err) {
              window.alert("사진을 불러올 수 없습니다.");
              throw err;
            }
          }
        });
      }
    }
  };
  • options, previewOptions을 나눈 이유 : options 값대로 압축해서 미리보기 이미지를 보여주면 여전히 입력창 딜레이가 발생했고 previewOptions에 맞춰서 이미지를 등록하면 화질 저하가 심해져졌다. 게시글 업로드 후 게시글 목록에서 데이터를 불러올 때 서버에서도 최적화되어 들어오니 용량이 더 줄어들어서 미리보기용 option을 따로 만들었다.

효과

이미지 리사이징 전

이미지리사이징_원본

  • 원본 1번 : 4.2MB (420ms)
  • 원본 2번 : 5.0MB (415ms)
  • 원본 3번 : 2.5MB (255ms)

이미지 리사이징 후 (서버 이미지 최적화하기 전)

이미지리사이징_1차

  • 리사이징 1번 : 2.5MB ➡️ 811KB (138ms)
  • 리사이징 2번 : 2.3MB ➡️ 835KB (123ms)
  • 리사이징 3번 : 2.2MB ➡️ 873KB (137ms)
  • 리사이징 4번 : 3.2MB ➡️ 1.1MB (261ms)

✅ 이미지 로딩 시간이 약 절반 가량 줄었다.

예상치 못한 버그 발생

정식 배포 후 팀원들의 지인 위주로 유저 테스트 진행 중 업로드할 이미지를 선택하면 간헐적으로 사진이 회전+축소된다는 버그 제보를 받았다.

browser-image-compression 이미지회전
출처 : https://github.com/Donaldcwl/browser-image-compression/issues/189
(제보 받은 사진이 개인적인 사진이라 동일한 이슈로 대체함)

자세히 물어보니 다음과 같은 상황이었다.

  • 갤럭시 S10 사용
  • 카카오톡에서 공유받은 url로 접속함 (카카오톡 브라우저)
  • 1024*1024 이미지 업로드 중 문제가 발생했으나 동일한 다른 사이즈의 이미지를 업로드했을 때는 문제없이 업로드 됨
  • 이후 갤럭시 기종을 사용하는 다른 팀원도 같은 문제를 겪은 적 있었음 (일시적인 현상으로 생각하고 넘어갔던 것 같다)

세 번째에서 당황.. 이미지크기 or 용량 or 확장자 이슈일 거라고 예측했지만 같은 이미지를 다른 갤럭시 기기에서 올리면 정상적으로 업로드 되었기 때문이다. 크롬 브라우저에서도 잘 업로드 되었다.
이미지 문제가 아니라면 브라우저 또는 안드로이드 기기에서 발생하는 버그가 아닐까 해서 구글링했는데 이런 문제를 겪는 케이스를 찾지 못했다.
그래서 라이브러리의 GitHub에 찾아가서 issue 탭에 가보니 나와 같은 문제를 겪은 issue가 있었다. (상단 사진과 url)

안드로이드 기기에서 간헐적으로 발생하는 이슈 같았고 아래 개발자의 comment를 보니 해결된 것 같지 않아 라이브러리를 교체하기로 했다.
(작년에 만든 프로젝트에서도 사용한 라이브러리였고 당시 업로드 관련 어떤 버그 제보도 없었다. 위 이슈도 올해 4월 20일에 올라온 걸 보면 올해부터 버그가 생긴 걸수도..?)

해결 (2) - react-image-file-resizer

browser-image-compression 대신 react-image-file-resizer 라이브러리를 사용했다.

선택 이유

  • browser-image-compression 다음으로 많은 사용자 + 최근 업데이트
  • issue에서도 큰 문제는 보이지 않았음

code

const resizeFile = (file: File) =>
    new Promise((res) => {
      Resizer.imageFileResizer(
        file, // target file
        1500, // maxWidth
        1500, // maxHeight
        "JPEG", // compressFormat : Can be either JPEG, PNG or WEBP.
        80, // quality : 0 and 100. Used for the JPEG compression
        0, // rotation
        (uri) => res(uri), // responseUriFunc
        "file" // outputType : Can be either base64, blob or file.(Default type is base64)	
      );
    });

  const resizePreview = (file: File) =>
    new Promise((res) => {
      Resizer.imageFileResizer(
        file,
        1500,
        1500,
        "JPEG",
        60,
        0,
        (uri) => res(uri),
        "base64"
      );
    });

  const handleAddImg = async (e: ChangeEvent<HTMLInputElement>) => {
    const target = e.target.files;

    if (target && target.length) {
      if (target.length + previewList.length > DISK_IMG_MAX_LENGTH) {
        // 이미지 최대 개수 제한
        window.alert(
          `사진은 최대 ${DISK_IMG_MAX_LENGTH}개까지 등록할 수 있습니다.`
        );
      } else {
        const newFiles: File[] = Array.from(target);
        newFiles.map(async (file) => {
          if (file.size > IMG_MAX_SIZE) {
            // 이미지 최대 용량 제한
            window.alert(
              `${Math.round(
                IMG_MAX_SIZE / 1000000
              )}MB 이하의 사진만 등록할 수 있습니다.`
            );
          } else {
            try {
              const compressedFile = (await resizeFile(file)) as File;
              const compressedPreview = (await resizePreview(file)) as string;
              setFiles((prev) => [...prev, compressedFile]);
              setPreviewList((prev: DiskImgType[]) => [
                ...prev,
                { imgId: "new", imgUrl: compressedPreview },
              ]);
            } catch (err) {
              window.alert("사진을 불러올 수 없습니다.");
              throw err;
            }
          }
        });
      }
    }
  };
  • 위와 마찬가지로 실제 업로드할 파일과 미리보기용 option을 분리했다.
  • 다만 react-image-file-resizer는 이미지 최대 용량이 아닌 quality option을 %로 정해야 했다. 이미지가 작으면 quality 100이어도 화질 저하가 심해 크기를 1500으로 키우는 대신 quality를 80으로 낮췄다.

효과

2차 이미지 리사이징 후 (업로드 화면)

리사이징 이후 입력창 딜레이

이미지리사이징_2차_업로드

  • 리사이징 1번 : 2.3MB ➡️ 249KB (미리보기 162KB)
  • 리사이징 2번 : 2.2MB ➡️ 286KB (미리보기 191KB)
  • 리사이징 3번 : 2.5MB ➡️ 287KB (미리보기 184KB)
  • 리사이징 4번 : 3.2MB ➡️ 405KB (미리보기 256KB)

2차 이미지 리사이징 후 (게시글 목록 - 서버 리사이징 이후)

리사이징 이후 목록

이미지리사이징_2차_목록

  • 리사이징 1번 : 156KB (76ms)
  • 리사이징 2번 : 184KB (90ms)
  • 리사이징 3번 : 174KB (84ms)
  • 리사이징 4번 : 240KB (88ms)

✅ 라이브러리 변경 후 시각적으로 큰 차이는 없었다.
✅ 기존 리사이징보다 1.6~2.9 배 로딩 속도가 빨라졌다. (게시글 목록 기준)
✅ 기존 리사이징 보다 약 1/4 이미지 용량을 줄였다. (업로드 페이지 기준)

후기

변경된 라이브러리 또한 유저 피드백을 기다려봐야겠지만 아직까진 문제없이 사용 중이다.
오히려 화질이 손상되지 않으면서 이미지 용량이 줄어 로딩 속도가 개선되었다.

참조

https://www.npmjs.com/package/browser-image-compression
browser-image-compression issue
https://github.com/onurzorluer/react-image-file-resizer

profile
FE Developer

0개의 댓글