본문 바로가기
프로젝트 답지

[Dap JI - issue] 이미지 업로드

by 띠리에이터 2024. 8. 26.
1. 구현 방법 및 고민
  • 다중 이미지 업로드 기능에서 동일한 이미자가 두 번 추가되는 문제 발생

 

  • FileReader를 사용해 미리보기 URL을 생성하는 동시에 서버로 파일을 업로드 한 후에도 다시 URL을 추가하는 과정에서 발생한 문제인 것 같다. 
2. 이슈
  • 사용자가 이미지를 업로드할 때 FileReader를 사용해 이미지의 미리보기 URL을 생성하고 서버에 파일을 업로드 한 후에도 setFileuUrl을 호출해 이미지 url을 상태에 추가했다. 이로인해 동일한 이미지가 두 번 추가된다. 
  • 미리보기 url과 실제 서버에서 반환된 url을 동일한 state로 관리하면서 발생했기 때문이다. 
const { mutate: boardImageUpload } = useMutation({
  mutationKey: ['boardImageUpload'],
  mutationFn: async (image: FormData) => {
    const res = await instance.post(`/api/----`, image);
    return res.data;
  },
  onSuccess: (data) => {
    setFileUrl((prev: string[]) => [...prev, data.imageUrl]);
    // 서버 응답 후 이미지 URL 추가
  },
  onError: (error) => {
    console.error('파일 업로드 실패:', error);
  },
});

const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
  const files = e.target.files;
  if (files && files.length > 0) {
    Array.from(files).forEach((file) => {
      const reader = new FileReader();
      reader.onloadend = () => {
        if (reader.result) {
          // 미리보기 URL 추가
          setFileUrl((prev: string[]) => [...prev, reader.result as string]);
        }
      };
      reader.readAsDataURL(file); // 파일을 base64로 인코딩
      const formData = new FormData();
      formData.append('image', file);
      boardImageUpload(formData); // 서버로 파일 업로드
    });
  }
};

 

3. 개선 방안 및 구현

 

  • 두가지 방법을 고려했다. 

  • 첫번째는 FileReader로 생성된 미리보기 URL을 상태에 추가하고, 서버 응답에서 URL을 다시 추가하지 않도록 하는 것, 즉, 서버에서 반환된 URL을 따로 상태에 추가하지 않는 방식.  단순하게 미리보기 URL만 관리하도록 하고, 서버 응답에서는 추가적인 상태 업데이트를 제거한다.

    url을 두번 추가하는 방식에서 문제가 발생했으니 데이터 패칭을 하는 곳에서 받은 url을 상태에 추가하지 않으면 되겠다! 하고 단순하게 생각했지만 url을 담을 곳이 없었다... 어이가 없는 너무 단순한 문제.. https~~ 로 시작하는 이미지 url이 필요한데 이걸 데이터 패칭하는 곳에서 못받아오니 인코딩된 데이터 url만 받아와졌다. 

 

  • 두번째는 미리보기의 state와 최종 url을 받는 state를 분리. 
    미리보기 url은 FileReader를 사용해 previewUrls라는 state에 저장하고 서버에서 응답받은 url은 fielUrl에 저장한다. 
    두가지 상태를 분리해서 관리하니 명확하게 데이터의 흐름을 유지할 수 있다. 

    일단 미리보기의 상태를 추가하는 state를 배열로 하나 생성, 
  const [previewUrls, setPreviewUrls] = useState<string[]>([]); 
  // 미리보기 상태 추가
  • useMutation의  onSucess 에서 미리보기 URL을 서버에서 반환된 실제 URL로 대체. 이를 통해 최종 업로드된 이미지의 URL만 fileUrl 에 유지
  • previewUrls 배열을 순회하면서, data.imageUrls 배열에 해당하는 인덱스에 값이 있으면 그 값으로 업데이트하고, 그렇지 않으면 기존의 previewUrls  배열 값을 그대로 유지
const { mutate: boardImageUpload } = useMutation({
  mutationKey: ['boardImageUpload'],
  mutationFn: async (image: FormData) => {
    const res = await instance.post(`/api/----`, image)
    return res.data;
  },
  onSuccess: (data) => {
    // 서버에서 받은 최종 URL을 미리보기 URL과 대체
    setFileUrl((prev: string[]) => [
      ...prev,
      ...data.imageUrls,
    ]);
    setPreviewUrls((prev) =>
      prev.map((url, index) => data.imageUrls[index] || url),
    );
  },
  onError: (error) => {
    console.error('파일 업로드 실패:', error);
  },
});
  • 그 후에 input onChange에 미리보기를 실행할 함수안에 setPreviews에 새로운 미리보기 url을 상태에 담에준다 

const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
  const files = e.target.files;
  if (files && files.length > 0) {
    Array.from(files).forEach((file) => {
      const reader = new FileReader();
      reader.onloadend = () => {
        if (reader.result) {
          // 새로운 미리보기 URL을 상태에 추가
          setPreviewUrls((prev: string[]) => [
            ...prev,
            reader.result as string,
          ]);
        }
      };
      reader.readAsDataURL(file); // 파일을 base64로 인코딩
      const formData = new FormData();
      formData.append('image', file);
      boardImageUpload(formData); // 서버로 파일 업로드
    });
  }
};

 

4. 결론

 

  • 좀 더 깔끔하게 하는 다른 방법이 있을 것 같다. 지금 방법은 돌아가는 느낌 ? 
     이미지를 다루는 부분은 아직도 상당히 헷갈린다. 특히 미리보기가 추가된다면....지금은 state를 두개로 분리해 관리를 하고 있지만 previews state를 사용하지 않고도 할 수 있는 방법이 있을까 더 고민해봐야겠다.