Life is connecting the dots .

이미지 업로드 기능 사용성[UI/UX] 개선 본문

Programming/React

이미지 업로드 기능 사용성[UI/UX] 개선

soyeori 2023. 9. 1. 21:12

이번주는 기존에 완성한 개인 프로젝트를 다시 점검하는 시간을 가졌다.👩‍🔧 기능 동작에 이상이 없는지 재점검하고 리팩토링한 코드를 다시 리팩토링도 해보았다. 이 중 사용자가 게시물을 등록하기 위해 작성하는 양식 중 이미지 업로드 기능을 확인했을 때, 기능은 잘 동작하지만 사용성을 조금 더 개선하고자 계획하였다.

현재 이미지 업로드 기능 동작 

문제 상황

❏ 업로드를 클릭한 공간에 이미지가 업로드된다.

❏ 이미지 수정은 가능하지만 삭제가 안된다.

❏ 이미지 업로드 완료까지 다소 시간이 걸린다.

challenge

❏ 어떤 업로드 버튼을 클릭하던 왼쪽부터 순서대로 사진 업로드 하기

❏ 업로드한 이미지 삭제 구현하기 (+. 삭제해도 이미지는 좌측정렬을 유지)

이미지 업로드 중인 동안 대체 UI 보여주기

 

이미지 업로드 로직을 간단히 설명하자면, [게시물 등록] 컴포넌트에서는 총 3개의 파일 URL 배열(ex. [ "image1.jpg", "image2.jpg", "image3.jpg" ])을 가지고 있고, 각각을 <UploadFile /> 컴포넌트로 뿌려준다. 실제 업로드 기능과 UI에 표시되는 부분은 [UploadFile] 컴포넌트에서 담당하는데, 해당 컴포넌트에서는 뮤테이션을 통해 이미지를 업로드한다. 업로드에 성공하면 그 파일 URL과 props로 전달받은 index를 다시 onChangeFileUrls에 인자로 전달해서 빈 배열에 각 index 위치에 파일 url을 채워준다. 

 

[게시물 등록] 컴포넌트

 // src/components/units/boards/BoardRegister.tsx
 export default function BoardRegister() {
  ...
  const [fileUrls, setFileUrls] = useState<string[]>(["", "", ""]);
  
  const onChangeFileUrls = (fileUrl: string, index: number) => {
    const newFileUrls = [...fileUrls]; // ["", "", ""]
    newFileUrls[index] = fileUrl; 
    setFileUrls(newFileUrls);
  };
  
  return (
    <div>
       {fileUrls.map((filrUrl, index) => (
         <UploadFile
           key={uuidv4()}
           fileUrl={fileUrl} 
           index={index}
           onChangeFileUrls={onChangeFileUrls}
         />
       ))} 
    </div>
   )
  }

[UploadFile] 컴포넌트 - 업로드 기능과 UI에 표시되는 부분

 // src/components/commons/Upload.tsx
export default function UploadFile(props: IUploadFileProps) {
  ...
  const [uploadFile] = useMutation<Pick<IMutation, "uploadFile">,IMutationUploadFileArgs>(UPLOAD_FILE);

  const onChangeFile = async (event: ChangeEvent<HTMLInputElement>) => {
    const file = UploadValidation(event.target.files?.[0]); // UploadValidation => 이미지 검증 로직
    
    try {
      const result = await uploadFile({ variables: { file } });
      props.onChangeFileUrls(String(result.data?.uploadFile.url), props.index);
    } catch (error) {
      if (error instanceof Error) {
        Modal.error({ content: error.message });
      }
    }
  };

  return (
	// 업로드 버튼과 이미지가 보이는 UI
  );
}

✅ 어떤 업로드 버튼을 클릭하던 왼쪽부터 사진 업로드 하기

지금은 파일 URL을 배열에 넣어줄 때 각 index 위치에 맞게 넣어주었는데, 왼쪽부터 순서대로 들어가게 하기 위해 배열을 순회하면서 값이 없는 위치에 파일 URL을 넣어주는 것으로 수정했다. 대신, 기존에 값이 있다면(이미지가 있다면) 그 이미지를 덮어씌우는 것으로 조건을 추가했다.

 // src/components/units/boards/BoardRegister.tsx
const onChangeFileUrls = (fileUrl: string, index: number) => {
  const newFileUrls = [...fileUrls];
  if (newFileUrls[index]) {
    newFileUrls[index] = fileUrl;
  } else {
    for (let i = 0; i < newFileUrls.length; i++) {
      if (!newFileUrls[i]) {
        newFileUrls[i] = fileUrl;
        break;
      }
    }
  }
  setFileUrls(newFileUrls);
};

 

두번째, 세번째 버튼에 업로드 했을 때(hover시 파랑색으로 테두리) 왼쪽부터 사진이 넣어짐

✅ 업로드한 이미지 삭제 구현하기 (+. 삭제해도 이미지는 좌측정렬을 유지)

► UI

업로드한 이미지에 마우스를 올렸을 때 휴지통 아이콘이 표시되는 화면을 먼저 만들어 주었다. 이미지 컨테이너 크기와 똑같은 사이즈의 컨테이너를 만들고, 처음에는 투명하게 보였다가 마우스를 올리면 불투명하게 변하도록 적용하였다. 휴지통 아이콘은 Ant-design Icon을 사용하여 색상과 크기를 재조정해 주었다.

► 삭제 기능

사용자가 휴지통 아이콘을 클릭했을 때 필터링을 통해 그 인덱스 값을 제외한 배열을 저장해 주면 되겠다는 생각으로 코드를 작성해 보았다. 이때 해당 인덱스는 어떻게 가지고 올 수 있을까? 처음에 이미지를 업로드할 때 props로 전달한 index를 사용해서 삭제 기능 함수의 인자로 넣어주면 된다.

이후 deleteFileByIndex 함수를 props로 [Upload] 컴포넌트까지 전달하고, 휴지통 아이콘에 클릭이벤트가 발생했을 때 해당 함수를 호출하고 인자로 index를 넣어주는 것으로 최종 완성시켰다. 또한 삭제 시 배열 자체의 길이가 줄어드는 문제는 반복문을 사용해서 초기 배열의 길이(3개)가 유지되도록 하여 해결하였다.

중간의 이미지가 삭제되어도 filter를 할 때 다시 처음부터 배열이 생성되므로 삭제 후에 왼쪽부터 이미지가 다시 정렬된다. 

 // src/components/units/boards/BoardRegister.tsx
const deleteFileByIndex = (index: number) => {
  const result = fileUrls.filter((_, fileUrlIndex) => fileUrlIndex !== index);
  const fileurls = [];
  for (let i = 0; i < fileUrls.length; i++) {
    result[i] ? fileurls.push(result[i]) : fileurls.push("");
  }
  setFileUrls(fileurls);
};

✅ 이미지 업로드 중인 동안 대체 UI 보여주기

이 과정에서 React <Suspense>를 사용해 볼까 생각했었지만 hook을 이용해서 로딩 중인 상태 여부에 따라 다른 UI를 보여주는 게 좀 더 디테일한 조건에서 사용하기에 유용하지 않을까라는 생각에 후자를 선택했다.

UI에서는 삼항연산자를 사용해서 (1) 전달받은 값이 있다면 이미지를 보여주고, (2-1) 값이 비었는데 isLoading이 true이면 로딩 중 이미지를, (2-2) 값이 비었는데 isLoading이 false이면 upload버튼을 보여주었다.

 // src/components/commons/Upload.tsx
export default function UploadFile(props: IUploadFileProps) {
  const [isLoading, setIsLoading] = useState(false);

  const onChangeFile = async (event: ChangeEvent<HTMLInputElement>) => {
    const file = UploadValidation(event.target.files?.[0]); // UploadValidation => 이미지 검증 로직
    setIsLoading(true); // 추가
    try {
      const result = await uploadFile({ variables: { file } });
      props.onChangeFileUrls(String(result.data?.uploadFile.url), props.index);
      setIsLoading(false); // 추가
    } catch (error) {
      if (error instanceof Error) {
        Modal.error({ content: error.message });
        setIsLoading(false); // 추가
      }
    }
  };

  return (
	// 업로드 버튼과 이미지가 보이는 UI
  );
}

🛠️ 향후 개선하면 좋을 부분

처음 목표로 세워두었던 부분을 모두 완료하였다. 조금 수정한 것만으로도 사용자 입장에서 훨씬 더 나은 사용성을 높여줄 수 있다. 또한 지금은 사진을 첨부하는 버튼이 총 3개로 되어있지만 다음에는 버튼을 한개만 두고 사용자가 업로드를 했을 때 추가 버튼이 만들어지고, 일정 개수에 도달하면 더 이상 버튼이 보이지 않는 방향으로 개선해도 좋을 것 같다. 그러면 사용자 입장에서 원하는 만큼의 UI만 볼 수 있게 되고, 기능 동작을 예측하기가 쉬울뿐더러 인터랙티브 한 느낌을 줄 수 있을 것 같다.