24/1/29

Laejun Kim·2024년 1월 29일
0

TIL

목록 보기
84/89

팀 프로젝트

진행 상황

드디어 첫 배포를 했다!
사실 배포 자체는 한지 좀 되었지만 실제 유저 피드백을 받기위해 필수 기능을 전부 구현해서 배포한 것은 오늘이 처음이다.

다음은 배포 링크이다 Baple

아직 많지는 않지만 실제 유저 피드백도 벌써부터 들어오고 있어서 너무 신기하다. 유저 피드백중에 유용한 것들을 잘 추려서 팀원들과 상의후 웹 어플리케이션 개선에 활용하면 더 좋은 서비스를 만들 수 있을것 같다.

Problem

사진 업로드 사이즈 제한 문제

기존에는 사진 업로드 파일 사이즈를 1mb로 제한해 둔 상태였다. 그 이유는 supabase 무료 plan을 사용하고 있기 때문에 사용 가능한 storage 용량이 제한적이었기 때문이다. 업로드 되는 사진의 크기를 제한하지 않으면 금방 storage가 꽉 찰 것이 분명했다.

그런데 모바일 테스트중 심각한 문제가 발생하였다. 모바일에서 사진 업로드를 시도했을 경우 휴대폰의 갤러리까지는 잘 열리는데 어떤 사진도 업로드가 불가능했던 것이다. 그리고 그 이유는 휴대폰으로 직접 찍은 사진중 1mb 를 초과하지 않는 것이 거의 없다는 점에 있었다.

결국 파일 업로드할때 업로드할 파일의 용량을 제한하는 방법은 애초에 잘못 되었던 것이다. 업로드할 용량은 자유롭게 하되, 업로드된 파일을 서버에 저장하기 전에 압축하여 크기를 1mb 이하로 줄일 방법이 필요했다.

browser-image-compression 라이브러리

이 문제를 해결하기 위한 방법을 찾아본 결과 이미 대부분의 웹 어플리케이션에서 사진 업로드시 자체적으로 사진의 용량을 적절한 사이즈로 압축하는 기술을 사용하고 있다는 것을 알게 되었다. 크게 사진 압축을 클라이언트단에서 하는 방식과, 서버에서 하는 방식, 이렇게 두가지 방식이 존재하였는데 지금 프로젝트에서는 자체 서버를 사용하고 있지 않기 때문에 서버에서 사진을 압축하는 방식은 구현하기 어렵겠다고 판단, 클라이언트에서 사진을 압축할 수 있는 방법을 찾아 보았다. 그리고 검색끝에 찾은 라이브러리가 browser-image-compression 라는 이름의 라이브러리이다.

browser-image-compression는 브라우저에서 실행되는 javascript 모듈로서 jpeg, png, webp, bmp 이미지를 서버에 업로드 하기 전 압축하는 역할을 한다. 옵션을 통해 멀티쓰레드를 활용할 수 있으며 async-await 문법을 이용해 간단하게 적용할 수 있다.

browser-image-compression 적용

  const handleImageChange = async (e: ChangeEvent<HTMLInputElement>) => {
    const files = e.target.files;
    const selectedImageArray = [...selectedImages];
    const selectedFileArray = [...selectedFiles];

    if (files) {
      for (let i = 0; i < files.length; i++) {
        const file = files[i];

        try {
          const options = {
            maxSizeMB: 1, //storage 에 들어가는 사진은 절대로 1mb를 넘지 않음(확인완료)
            maxWidthOrHeight: 1920,
            useWebWorker: true, // 멀티쓰레드 사용 여부. 사용못할시 자동으로 싱글쓰레드로 동작
          };

          const compressedFile = await imageCompression(file, options);
          const imageUrl = URL.createObjectURL(compressedFile);

          if (selectedImageArray.length < 3) {
            selectedImageArray.push({ file: compressedFile, imageUrl });
            selectedFileArray.push(compressedFile);
          } else {
            toastWarn('이미지는 최대 3장만 업로드 가능합니다.');
          }
        } catch (error) {
          console.error('Error compressing image:', error);
          toastError('이미지 압축 중 오류가 발생했습니다.');
        }
      }

      setSelectedImages(selectedImageArray);
      setSelectedFiles(selectedFileArray);
    }
  };

다음은 기존에 존재하던 handleImageChange 함수에 browser-image-compression를 함께 적용한 모습이다.

간단하게 import imageCompression from 'browser-image-compression 로 불러올 수 있으며(물론 yarn 이나 npm으로 까는 것은 필수다) imageCompression 함수에 압축할 파일과 option 객체를 함께 전달함으로서 압축된 파일을 리턴 받는 것이 가능하다. 에러 핸들링을 위해 try-catch 문을 활용하였으며 에러 메시지 출력도 잘 되는 것을 확인하였다.

상술한 에러 메시지의 경우 갤럭시에서 테스트 했을때 발생했는데, 이 부분만큼은 현재로선 어쩔 도리가 없다.

browser-image-compression이 삼성 폰에서 열리는 카카오 인앱 브라우저를 지원하지 않아서 발생한 에러이기 때문이다.

그 외의 브라우저에서는 매끄럽게 잘 동작하는 것으로 확인하였다.

테스트 삼아 넣어본 이 이미지도 원본은 10mb 가 넘어가는 큰 이미지 파일이었는데 supabase 서버에는 1mb 이하의 크기로 저장되는 것도 확인하였다.

사진 업로드 지연 문제

대용량 사진 업로드 기능을 구현하고 나자 이전에 없었던 또 다른 문제가 발생하였다. 큰 용량의 사진을 업로드 하면 이를 압축을 하는데, 사진 압축이 꽤 리소스가 드는 작업이니 만큼 용량이 큰 사진일수록 리뷰 등록 버튼을 누르고 실제로 리뷰가 등록되기까지의 시간이 벌어지는 것이다.

등록하기 버튼을 누르고 한동안 아무런 반응이 없으므로 사용자는 웹사이트가 제대로 작동하고 있는 것인지 의심하게 되고 등록하기 버튼을 연타하는 경우가 발생했으며, 이렇게 여러번 입력된 경우 입력된 횟수만큼 동일한 내용의 리뷰가 중복해서 등록되는 문제가 관찰되었다. 또한 중복 등록 문제가 발생하지 않는다고 하여도 아무런 반응 없는 화면을 2초 이상 보여주는 것은 ux를 해치므로 무언가 해결책을 떠올려야 했다.

업로드 지연은 사진을 압축하는 과정에서 발생하는 것이므로 불가피한 상황. 따라서 이에 맞는 ui를 새로 만들기로 결정했다.

스피너 모달 도입

리뷰 등록이 길어질 경우 스피너가 있는 모달을 화면에 띄우는 것으로 문제를 해결 할 수 있을 것으로 판단하였다.

모달이 있으면 백드롭으로 화면의 다른 요소(등록하기 버튼)와의 상호작용을 완전히 차단할 수 있고 모달에 움직이는 요소(스피너)를 넣음으로써 기다리는 지루함을 반감시킬수 있을 것이라고 생각했기 때문이다.

import { Spacer, Spinner } from '@nextui-org/react';
import { useTheme } from 'next-themes';
import React from 'react';

const ReviewSubmitSpinner = () => {
  const { theme } = useTheme();
  return (
    <div
      className={`w-[100%] h-[100%] fixed  z-10 flex justify-center items-center`}
    >
      <div
        className={`z-20 bg-${
          theme === 'baple' ? 'white' : 'secondary'
        } rounded-md w-[300px] h-[200px] flex flex-col justify-center items-center opacity-100 absolute bg-opacity-100`}
      >
        <Spinner size='lg' />
        <Spacer y={10} />
        <p>업로드중.. 잠시만 기다려주세요 😜</p>
      </div>
      <div className='absolute w-[100%] h-[100%] bg-gray-900 opacity-50'></div>
    </div>
  );
};

export default ReviewSubmitSpinner;

위는 직접 만든 커스텀 모달 컴포넌트이다. 스피너는 nextui 것을 활용했으며 백드롭과 모달 표시영역을 함께 컴포넌트에 포장해 간단하게 활용할 수 있도록 만들었다. p태그 안에 들어가는 모달 메시지를 컴포넌트 prop으로 받게 변경한다면 리뷰 등록 페이지 뿐만 아니라 다른 곳에서도 활용할 수 있는 공용 컴포넌트로 만드는 것도 가능할 것이다.

웹 어플리케이션 전체에 적용되는 색맹 theme 도 존재하므로 모달도 이에 맞춰서 스타일이 자동으로 변경되도록 했으며 백드롭에는 opacity-50 을 주어 배경의 ui 는 모달이 떠있는 동안 상호작용이 불가능 하다는 것을 전달하고자 하였다.

백드롭에 blur 이펙트를 주는것도 고민했으나 이전 프로젝트에서 blur 속성을 주었을때 웹 어플리케이션의 속도가 느려졌던 경험이 있어 이번에는 도입하지 않았다.

이렇게 만든 모달 컴포넌트는 리뷰 입력 페이지 컴포넌트에서 아래와 같이 적용하였다.

//리뷰 등록 페이지에서 리뷰 등록을 담당하는 함수

  const onSubmitReview = async () => {
  🎇setModalOpen(true); // 스피너 모달 열기
    const publicUrlList: string[] = [];
    if (selectedFiles) {
      for (const file of selectedFiles) {
        const { data: fileData, error: fileError } = await supabase.storage
          .from('review_images')
          .upload(`${Date.now()}`, file);
        if (fileError) {
          console.error('이미지 업로드 에러', fileError.message);
          return;
        }
        const { data: imageData } = supabase.storage
          .from('review_images')
          .getPublicUrl(fileData.path);

        publicUrlList.push(imageData.publicUrl);
      }
      if (placeInfo?.image_url === null) {
        mutateToUpdate({ id: placeId as string, imageUrl: publicUrlList[0] });
      }
    }

    const isReviewEmpty = /^\s*$/;
    if (!reviewText) {
      toastWarn('후기는 필수 요소입니다. 입력 후 등록해 주세요.');
      return;
    } else if (isReviewEmpty.test(reviewText)) {
      toastWarn('공백 이외 내용을 입력해 주세요.');
      return;
    }
    const args = {
      content: reviewText,
      placeId: placeId as string,
      userId,
      publicUrlList,
    };
    insertReview(args);
  🎇setModalOpen(false); // 스피너 모달 닫기
    toastSuccess('리뷰가 등록되었습니다.');
    router.replace(`/place/${placeId}`);
  };

먼저 모달의 열고 닫힘을 결정하는 state를 하나 만들고 해당 state가 등록 버튼과 연결되어 있는 함수 초입에 true로 바뀌고, 다시 동일한 함수가 종료될 즈음에 false로 바뀌게 만들었다.

이렇게 함으로써 사용자가 모달의 열고 닫음을 결정하는 것이 아니라 웹 어플리케이션에서 특정 동작이 실행될때 그것이 '오래 걸릴 경우' 자동으로 모달이 나타나고 그 동작이 완료될때 모달이 함께 닫히도록 만들었다.

'오래 걸릴 경우'라고 한정하여 표현한 이유는 사진을 업로드하지 않았거나 사진이 저용량인 경우에는 위 함수가 거의 즉시 실행되어 모달이 화면에 표시되는것을 눈치채기조차 어렵기 때문이다.

모달 컴포넌트는 tsx 리턴부에 {modalOpen && <ReviewSubmitSpinner />} 이렇게 간단하게 조건부로 렌더링 되도록 만들어 두었다.

등록 스피너 모달이 적용된 모습은 다음과 같다.

대용량 사진도 자유롭게 업로드가 가능해짐과 동시에 관련 모달도 잘 만들어져서 대단히 만족스럽다

0개의 댓글